pax_global_header00006660000000000000000000000064140504364600014514gustar00rootroot0000000000000052 comment=602e329532049c9e33fa8c74e352a46fb9486947 reflow-0.3.0/000077500000000000000000000000001405043646000130125ustar00rootroot00000000000000reflow-0.3.0/.github/000077500000000000000000000000001405043646000143525ustar00rootroot00000000000000reflow-0.3.0/.github/FUNDING.yml000066400000000000000000000000171405043646000161650ustar00rootroot00000000000000github: muesli reflow-0.3.0/.github/workflows/000077500000000000000000000000001405043646000164075ustar00rootroot00000000000000reflow-0.3.0/.github/workflows/build.yml000066400000000000000000000017701405043646000202360ustar00rootroot00000000000000name: build on: [push, pull_request] jobs: test: strategy: matrix: go-version: [1.11.x, 1.12.x, 1.13.x, 1.14.x, 1.15.x] os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} env: GO111MODULE: "on" steps: - name: Install Go uses: actions/setup-go@v1 with: go-version: ${{ matrix.go-version }} - name: Checkout code uses: actions/checkout@v2 - name: Download Go modules run: go mod download - name: Build run: go build -v ./... - name: Test run: go test ./... - name: Coverage env: COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | go test -race -covermode atomic -coverprofile=profile.cov ./... GO111MODULE=off go get github.com/mattn/goveralls $(go env GOPATH)/bin/goveralls -coverprofile=profile.cov -service=github if: matrix.go-version == '1.15.x' && matrix.os == 'ubuntu-latest' reflow-0.3.0/.github/workflows/lint.yml000066400000000000000000000013361405043646000201030ustar00rootroot00000000000000name: lint on: push: pull_request: jobs: golangci: name: lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: golangci-lint uses: golangci/golangci-lint-action@v2 with: # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. version: v1.31 # Optional: golangci-lint command line arguments. args: --issues-exit-code=0 # Optional: working directory, useful for monorepos # working-directory: somedir # Optional: show only new issues if it's a pull request. The default value is `false`. only-new-issues: true reflow-0.3.0/.gitignore000066400000000000000000000003001405043646000147730ustar00rootroot00000000000000# Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, build with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out reflow-0.3.0/.golangci.yml000066400000000000000000000005351405043646000154010ustar00rootroot00000000000000run: tests: false issues: max-issues-per-linter: 0 max-same-issues: 0 linters: enable: - bodyclose - dupl - exportloopref - goconst - godot - godox - goimports - goprintffuncname - gosec - misspell - prealloc - rowserrcheck - sqlclosecheck - unconvert - unparam - whitespace reflow-0.3.0/LICENSE000066400000000000000000000020671405043646000140240ustar00rootroot00000000000000MIT License Copyright (c) 2019 Christian Muehlhaeuser Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. reflow-0.3.0/README.md000066400000000000000000000074531405043646000143020ustar00rootroot00000000000000# reflow [![Latest Release](https://img.shields.io/github/release/muesli/reflow.svg)](https://github.com/muesli/reflow/releases) [![Build Status](https://github.com/muesli/reflow/workflows/build/badge.svg)](https://github.com/muesli/reflow/actions) [![Coverage Status](https://coveralls.io/repos/github/muesli/reflow/badge.svg?branch=master)](https://coveralls.io/github/muesli/reflow?branch=master) [![Go ReportCard](http://goreportcard.com/badge/muesli/reflow)](http://goreportcard.com/report/muesli/reflow) [![GoDoc](https://godoc.org/github.com/golang/gddo?status.svg)](https://pkg.go.dev/github.com/muesli/reflow) A collection of ANSI-aware methods and `io.Writers` helping you to transform blocks of text. This means you can still style your terminal output with ANSI escape sequences without them affecting the reflow operations & algorithms. ## Word-Wrapping The `wordwrap` package lets you word-wrap strings or entire blocks of text. ```go import "github.com/muesli/reflow/wordwrap" s := wordwrap.String("Hello World!", 5) fmt.Println(s) ``` Result: ``` Hello World! ``` The word-wrapping Writer is compatible with the `io.Writer` / `io.WriteCloser` interfaces: ```go f := wordwrap.NewWriter(limit) f.Write(b) f.Close() fmt.Println(f.String()) ``` Customize word-wrapping behavior: ```go f := wordwrap.NewWriter(limit) f.Breakpoints = []rune{':', ','} f.Newline = []rune{'\r'} ``` ## Unconditional Wrapping The `wrap` package lets you unconditionally wrap strings or entire blocks of text. ```go import "github.com/muesli/reflow/wrap" s := wrap.String("Hello World!", 7) fmt.Println(s) ``` Result: ``` Hello W orld! ``` The unconditional wrapping Writer is compatible with the `io.Writer` interfaces: ```go f := wrap.NewWriter(limit) f.Write(b) fmt.Println(f.String()) ``` Customize word-wrapping behavior: ```go f := wrap.NewWriter(limit) f.Newline = []rune{'\r'} f.KeepNewlines = false f.reserveSpace = true f.TabWidth = 2 ``` **Tip:** This wrapping method can be used in conjunction with word-wrapping when word-wrapping is preferred but a line limit has to be enforced: ```go wrapped := wrap.String(wordwrap.String("Just an example", 5), 5) fmt.Println(wrapped) ``` Result: ``` Just an examp le ``` ### ANSI Example ```go s := wordwrap.String("I really \x1B[38;2;249;38;114mlove\x1B[0m Go!", 8) fmt.Println(s) ``` Result: ![ANSI Example Output](https://github.com/muesli/reflow/blob/master/reflow.png) ## Indentation The `indent` package lets you indent strings or entire blocks of text. ```go import "github.com/muesli/reflow/indent" s := indent.String("Hello World!", 4) fmt.Println(s) ``` Result: ` Hello World!` There is also an indenting Writer, which is compatible with the `io.Writer` interface: ```go // indent uses spaces per default: f := indent.NewWriter(width, nil) // but you can also use a custom indentation function: f = indent.NewWriter(width, func(w io.Writer) { w.Write([]byte(".")) }) f.Write(b) f.Close() fmt.Println(f.String()) ``` ## Dedentation The `dedent` package lets you dedent strings or entire blocks of text. ```go import "github.com/muesli/reflow/dedent" input := ` Hello World! Hello World! ` s := dedent.String(input) fmt.Println(s) ``` Result: ``` Hello World! Hello World! ``` ## Padding The `padding` package lets you pad strings or entire blocks of text. ```go import "github.com/muesli/reflow/padding" s := padding.String("Hello", 8) fmt.Println(s) ``` Result: `Hello___` (the underlined portion represents 3 spaces) There is also a padding Writer, which is compatible with the `io.WriteCloser` interface: ```go // padding uses spaces per default: f := padding.NewWriter(width, nil) // but you can also use a custom padding function: f = padding.NewWriter(width, func(w io.Writer) { w.Write([]byte(".")) }) f.Write(b) f.Close() fmt.Println(f.String()) ``` reflow-0.3.0/ansi/000077500000000000000000000000001405043646000137445ustar00rootroot00000000000000reflow-0.3.0/ansi/ansi.go000066400000000000000000000002051405043646000152220ustar00rootroot00000000000000package ansi const Marker = '\x1B' func IsTerminator(c rune) bool { return (c >= 0x40 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a) } reflow-0.3.0/ansi/buffer.go000066400000000000000000000013041405043646000155420ustar00rootroot00000000000000package ansi import ( "bytes" "github.com/mattn/go-runewidth" ) // Buffer is a buffer aware of ANSI escape sequences. type Buffer struct { bytes.Buffer } // PrintableRuneWidth returns the cell width of all printable runes in the // buffer. func (w Buffer) PrintableRuneWidth() int { return PrintableRuneWidth(w.String()) } // PrintableRuneWidth returns the cell width of the given string. func PrintableRuneWidth(s string) int { var n int var ansi bool for _, c := range s { if c == Marker { // ANSI escape sequence ansi = true } else if ansi { if IsTerminator(c) { // ANSI sequence terminated ansi = false } } else { n += runewidth.RuneWidth(c) } } return n } reflow-0.3.0/ansi/buffer_test.go000066400000000000000000000010631405043646000166030ustar00rootroot00000000000000package ansi import ( "bytes" "testing" ) func TestBuffer_PrintableRuneWidth(t *testing.T) { t.Parallel() var bb bytes.Buffer bb.WriteString("\x1B[38;2;249;38;114mfoo") b := Buffer{bb} if n := b.PrintableRuneWidth(); n != 3 { t.Fatalf("width should be 3, got %d", n) } } // go test -bench=Benchmark_PrintableRuneWidth -benchmem -count=4 func Benchmark_PrintableRuneWidth(b *testing.B) { s := "\x1B[38;2;249;38;114mfoo" b.RunParallel(func(pb *testing.PB) { b.ReportAllocs() b.ResetTimer() for pb.Next() { PrintableRuneWidth(s) } }) } reflow-0.3.0/ansi/writer.go000066400000000000000000000026201405043646000156070ustar00rootroot00000000000000package ansi import ( "bytes" "io" "unicode/utf8" ) type Writer struct { Forward io.Writer ansi bool ansiseq bytes.Buffer lastseq bytes.Buffer seqchanged bool runeBuf []byte } // Write is used to write content to the ANSI buffer. func (w *Writer) Write(b []byte) (int, error) { for _, c := range string(b) { if c == Marker { // ANSI escape sequence w.ansi = true w.seqchanged = true _, _ = w.ansiseq.WriteRune(c) } else if w.ansi { _, _ = w.ansiseq.WriteRune(c) if IsTerminator(c) { // ANSI sequence terminated w.ansi = false if bytes.HasSuffix(w.ansiseq.Bytes(), []byte("[0m")) { // reset sequence w.lastseq.Reset() w.seqchanged = false } else if c == 'm' { // color code _, _ = w.lastseq.Write(w.ansiseq.Bytes()) } _, _ = w.ansiseq.WriteTo(w.Forward) } } else { _, err := w.writeRune(c) if err != nil { return 0, err } } } return len(b), nil } func (w *Writer) writeRune(r rune) (int, error) { if w.runeBuf == nil { w.runeBuf = make([]byte, utf8.UTFMax) } n := utf8.EncodeRune(w.runeBuf, r) return w.Forward.Write(w.runeBuf[:n]) } func (w *Writer) LastSequence() string { return w.lastseq.String() } func (w *Writer) ResetAnsi() { if !w.seqchanged { return } _, _ = w.Forward.Write([]byte("\x1b[0m")) } func (w *Writer) RestoreAnsi() { _, _ = w.Forward.Write(w.lastseq.Bytes()) } reflow-0.3.0/ansi/writer_test.go000066400000000000000000000041751405043646000166550ustar00rootroot00000000000000package ansi import ( "bytes" "errors" "io/ioutil" "testing" ) func TestWriter_Write(t *testing.T) { t.Parallel() buf := []byte("\x1B[38;2;249;38;114m你好reflow\x1B[0m") forward := &bytes.Buffer{} w := &Writer{Forward: forward} n, err := w.Write(buf) w.ResetAnsi() w.RestoreAnsi() if err != nil { t.Fatalf("err should be nil, but got %v", err) } if l := len(buf); n != l { t.Fatalf("n should be %d, got %d", l, n) } if ls := w.LastSequence(); ls != "" { t.Fatalf("LastSequence should be empty, got %s", ls) } if b := forward.Bytes(); !bytes.Equal(b, buf) { t.Fatalf("forward should be wrote by %v, but got %v", buf, b) } } var fakeErr = errors.New("fake error") type fakeWriter struct{} func (fakeWriter) Write(_ []byte) (int, error) { return 0, fakeErr } func TestWriter_Write_Error(t *testing.T) { t.Parallel() w := &Writer{Forward: fakeWriter{}} _, err := w.Write([]byte("foo")) if err != fakeErr { t.Fatalf("err should be fakeErr, but got %v", err) } } // go test -bench=BenchmarkWriter_Write -benchmem -count=4 func BenchmarkWriter_Write(b *testing.B) { buf := []byte("\x1B[38;2;249;38;114m你好reflow\x1B[0m") w := &Writer{Forward: ioutil.Discard} b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { _, _ = w.Write(buf) } } func TestWriter_LastSequence(t *testing.T) { t.Parallel() w := &Writer{} if s := w.LastSequence(); s != "" { t.Fatalf("LastSequence should be empty, but got %s", s) } } func TestWriter_ResetAnsi(t *testing.T) { t.Parallel() b := &bytes.Buffer{} w := &Writer{Forward: b} w.ResetAnsi() if s := b.String(); s != "" { t.Fatalf("b should be empty, but got %s", s) } w.seqchanged = true w.ResetAnsi() if s := b.String(); s != "\x1b[0m" { t.Fatalf("b.String() should be \"\\x1b[0m\", got %s", s) } } func TestWriter_RestoreAnsi(t *testing.T) { t.Parallel() b := &bytes.Buffer{} lastseq := bytes.Buffer{} lastseq.WriteString("\x1B[38;2;249;38;114m") w := &Writer{Forward: b, lastseq: lastseq} w.RestoreAnsi() if s := b.String(); s != "\x1B[38;2;249;38;114m" { t.Fatalf("b.String() should be \"\\x1B[38;2;249;38;114m\", got %s", s) } } reflow-0.3.0/dedent/000077500000000000000000000000001405043646000142555ustar00rootroot00000000000000reflow-0.3.0/dedent/dedent.go000066400000000000000000000022141405043646000160460ustar00rootroot00000000000000package dedent import ( "bytes" ) // String automatically detects the maximum indentation shared by all lines and // trims them accordingly. func String(s string) string { indent := minIndent(s) if indent == 0 { return s } return dedent(s, indent) } func minIndent(s string) int { var ( curIndent int minIndent int shouldAppend = true ) for i := 0; i < len(s); i++ { switch s[i] { case ' ', '\t': if shouldAppend { curIndent++ } case '\n': curIndent = 0 shouldAppend = true default: if curIndent > 0 && (minIndent == 0 || curIndent < minIndent) { minIndent = curIndent curIndent = 0 } shouldAppend = false } } return minIndent } func dedent(s string, indent int) string { var ( omitted int shouldOmit = true buf bytes.Buffer ) for i := 0; i < len(s); i++ { switch s[i] { case ' ', '\t': if shouldOmit { if omitted < indent { omitted++ continue } shouldOmit = false } _ = buf.WriteByte(s[i]) case '\n': omitted = 0 shouldOmit = true _ = buf.WriteByte(s[i]) default: _ = buf.WriteByte(s[i]) } } return buf.String() } reflow-0.3.0/dedent/dedent_test.go000066400000000000000000000027341405043646000171140ustar00rootroot00000000000000package dedent import ( "testing" ) func TestDedent(t *testing.T) { tt := []struct { Input string Expected string }{ { Input: " --help Show help for command\n --version Show version\n", Expected: "--help Show help for command\n--version Show version\n", }, { Input: " --help Show help for command\n -C, --config string Specify the config file to use\n", Expected: " --help Show help for command\n-C, --config string Specify the config file to use\n", }, { Input: " line 1\n\n line 2\n line 3", Expected: " line 1\n\n line 2\nline 3", }, { Input: " line 1\n line 2\n line 3\n\n", Expected: "line 1\nline 2\nline 3\n\n", }, { Input: " \tline 1\n\t\tline 2\n\t line 3\n\n", Expected: "line 1\nline 2\nline 3\n\n", }, { Input: "\t\tline 1\n\n\t\tline 2\n\tline 3", Expected: "\tline 1\n\n\tline 2\nline 3", }, { Input: "\n\n\n\n\n\n", Expected: "\n\n\n\n\n\n", }, { Input: "", Expected: "", }, } for i, tc := range tt { s := String(tc.Input) if s != tc.Expected { t.Errorf("Test %d, expected:\n\n`%s`\n\nActual Output:\n\n`%s`", i, tc.Expected, s) } } } // go test -bench=BenchmarkDedent -benchmem -count=4 func BenchmarkDedent(b *testing.B) { b.RunParallel(func(pb *testing.PB) { input := " line 1\n\n line 2\n line 3" b.ReportAllocs() b.ResetTimer() for pb.Next() { String(input) } }) } reflow-0.3.0/go.mod000066400000000000000000000002101405043646000141110ustar00rootroot00000000000000module github.com/muesli/reflow go 1.13 require ( github.com/mattn/go-runewidth v0.0.12 github.com/rivo/uniseg v0.2.0 // indirect ) reflow-0.3.0/go.sum000066400000000000000000000006531405043646000141510ustar00rootroot00000000000000github.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxmAOow= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= reflow-0.3.0/indent/000077500000000000000000000000001405043646000142735ustar00rootroot00000000000000reflow-0.3.0/indent/indent.go000066400000000000000000000043031405043646000161030ustar00rootroot00000000000000package indent import ( "bytes" "io" "strings" "github.com/muesli/reflow/ansi" ) type IndentFunc func(w io.Writer) type Writer struct { Indent uint IndentFunc IndentFunc ansiWriter *ansi.Writer buf bytes.Buffer skipIndent bool ansi bool } func NewWriter(indent uint, indentFunc IndentFunc) *Writer { w := &Writer{ Indent: indent, IndentFunc: indentFunc, } w.ansiWriter = &ansi.Writer{ Forward: &w.buf, } return w } func NewWriterPipe(forward io.Writer, indent uint, indentFunc IndentFunc) *Writer { return &Writer{ Indent: indent, IndentFunc: indentFunc, ansiWriter: &ansi.Writer{ Forward: forward, }, } } // Bytes is shorthand for declaring a new default indent-writer instance, // used to immediately indent a byte slice. func Bytes(b []byte, indent uint) []byte { f := NewWriter(indent, nil) _, _ = f.Write(b) return f.Bytes() } // String is shorthand for declaring a new default indent-writer instance, // used to immediately indent a string. func String(s string, indent uint) string { return string(Bytes([]byte(s), indent)) } // Write is used to write content to the indent buffer. func (w *Writer) Write(b []byte) (int, error) { for _, c := range string(b) { if c == '\x1B' { // ANSI escape sequence w.ansi = true } else if w.ansi { if (c >= 0x41 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a) { // ANSI sequence terminated w.ansi = false } } else { if !w.skipIndent { w.ansiWriter.ResetAnsi() if w.IndentFunc != nil { for i := 0; i < int(w.Indent); i++ { w.IndentFunc(w.ansiWriter) } } else { _, err := w.ansiWriter.Write([]byte(strings.Repeat(" ", int(w.Indent)))) if err != nil { return 0, err } } w.skipIndent = true w.ansiWriter.RestoreAnsi() } if c == '\n' { // end of current line w.skipIndent = false } } _, err := w.ansiWriter.Write([]byte(string(c))) if err != nil { return 0, err } } return len(b), nil } // Bytes returns the indented result as a byte slice. func (w *Writer) Bytes() []byte { return w.buf.Bytes() } // String returns the indented result as a string. func (w *Writer) String() string { return w.buf.String() } reflow-0.3.0/indent/indent_test.go000066400000000000000000000053301405043646000171430ustar00rootroot00000000000000package indent import ( "bytes" "errors" "io" "testing" "github.com/muesli/reflow/ansi" ) func TestIndent(t *testing.T) { t.Parallel() tt := []struct { Input string Expected string Indent uint }{ // No-op, should pass through: { "foobar", "foobar", 0, }, // Basic indentation: { "foobar", " foobar", 4, }, // Multi-line indentation: { "foo\nbar", " foo\n bar", 4, }, // ANSI sequence codes: { "\x1B[38;2;249;38;114mfoo", "\x1B[38;2;249;38;114m\x1B[0m \x1B[38;2;249;38;114mfoo", 4, }, } for i, tc := range tt { f := NewWriter(tc.Indent, nil) _, err := f.Write([]byte(tc.Input)) if err != nil { t.Error(err) } if f.String() != tc.Expected { t.Errorf("Test %d, expected:\n\n`%s`\n\nActual Output:\n\n`%s`", i, tc.Expected, f.String()) } } } func TestIndentWriter(t *testing.T) { t.Parallel() f := NewWriter(4, nil) _, err := f.Write([]byte("foo\n")) if err != nil { t.Error(err) } _, err = f.Write([]byte("bar")) if err != nil { t.Error(err) } exp := " foo\n bar" if f.String() != exp { t.Errorf("expected:\n\n`%s`\n\nActual Output:\n\n`%s`", exp, f.String()) } } func TestIndentString(t *testing.T) { t.Parallel() actual := String("foobar", 3) expected := " foobar" if actual != expected { t.Errorf("expected:\n\n`%s`\n\nActual Output:\n\n`%s`", expected, actual) } } func BenchmarkIndentString(b *testing.B) { b.RunParallel(func(pb *testing.PB) { b.ReportAllocs() b.ResetTimer() for pb.Next() { String("foo", 2) } }) } func TestIndentWriterWithIndentFunc(t *testing.T) { t.Parallel() f := NewWriter(2, func(w io.Writer) { _, _ = w.Write([]byte(".")) }) _, err := f.Write([]byte("foo\n")) if err != nil { t.Error(err) } _, err = f.Write([]byte("bar")) if err != nil { t.Error(err) } exp := "..foo\n..bar" if f.String() != exp { t.Errorf("expected:\n\n`%s`\n\nActual Output:\n\n`%s`", exp, f.String()) } } func TestNewWriterPipe(t *testing.T) { t.Parallel() b := &bytes.Buffer{} f := NewWriterPipe(b, 2, nil) if _, err := f.Write([]byte("foo")); err != nil { t.Error(err) } actual := b.String() expected := " foo" if actual != expected { t.Errorf("expected:\n\n`%s`\n\nActual Output:\n\n`%s`", expected, actual) } } func TestWriter_Error(t *testing.T) { t.Parallel() f := &Writer{ Indent: 2, ansiWriter: &ansi.Writer{Forward: fakeWriter{}}, } if _, err := f.Write([]byte("foo")); err != fakeErr { t.Error(err) } f.skipIndent = true if _, err := f.Write([]byte("foo")); err != fakeErr { t.Error(err) } } var fakeErr = errors.New("fake error") type fakeWriter struct{} func (fakeWriter) Write(_ []byte) (int, error) { return 0, fakeErr } reflow-0.3.0/margin/000077500000000000000000000000001405043646000142675ustar00rootroot00000000000000reflow-0.3.0/margin/margin.go000066400000000000000000000030341405043646000160730ustar00rootroot00000000000000package margin import ( "bytes" "io" "github.com/muesli/reflow/indent" "github.com/muesli/reflow/padding" ) type Writer struct { buf bytes.Buffer pw *padding.Writer iw *indent.Writer } func NewWriter(width uint, margin uint, marginFunc func(io.Writer)) *Writer { pw := padding.NewWriter(width, marginFunc) iw := indent.NewWriter(margin, marginFunc) return &Writer{ pw: pw, iw: iw, } } // Bytes is shorthand for declaring a new default margin-writer instance, // used to immediately apply a margin to a byte slice. func Bytes(b []byte, width uint, margin uint) []byte { f := NewWriter(width, margin, nil) _, _ = f.Write(b) f.Close() return f.Bytes() } // String is shorthand for declaring a new default margin-writer instance, // used to immediately apply a margin to a string. func String(s string, width uint, margin uint) string { return string(Bytes([]byte(s), width, margin)) } func (w *Writer) Write(b []byte) (int, error) { _, err := w.iw.Write(b) if err != nil { return 0, err } n, err := w.pw.Write(w.iw.Bytes()) if err != nil { return n, err } return n, nil } // Close will finish the margin operation. Always call it before trying to // retrieve the final result. func (w *Writer) Close() error { err := w.pw.Close() if err != nil { return err } _, err = w.buf.Write(w.pw.Bytes()) return err } // Bytes returns the result as a byte slice. func (w *Writer) Bytes() []byte { return w.buf.Bytes() } // String returns the result as a string. func (w *Writer) String() string { return w.buf.String() } reflow-0.3.0/margin/margin_test.go000066400000000000000000000037431405043646000171410ustar00rootroot00000000000000package margin import ( "errors" "testing" "github.com/muesli/reflow/indent" "github.com/muesli/reflow/padding" ) func TestMargin(t *testing.T) { tt := []struct { Input string Expected string Width uint Margin uint }{ // No-op, should pass through: { "foobar", "foobar", 0, 0, }, // Basic margin: { "foobar", " foobar ", 10, 2, }, // Asymmetric margin: { "foo", " foo ", 6, 2, }, // Multi-line margin: { "foo\nbar", " foo \n bar ", 5, 1, }, // Don't pad empty trailing lines: { "foo\nbar\n", " foo \n bar \n", 5, 1, }, // ANSI sequence codes: { "\x1B[38;2;249;38;114mfoo", "\x1B[38;2;249;38;114m\x1B[0m \x1B[38;2;249;38;114mfoo ", 9, 3, }, } for i, tc := range tt { f := NewWriter(tc.Width, tc.Margin, nil) _, err := f.Write([]byte(tc.Input)) if err != nil { t.Error(err) } f.Close() if f.String() != tc.Expected { t.Errorf("Test %d, expected:\n\n`%s`\n\nActual Output:\n\n`%s`", i, tc.Expected, f.String()) } } } func TestMarginString(t *testing.T) { actual := String("foobar", 10, 2) expected := " foobar " if actual != expected { t.Errorf("expected:\n\n`%s`\n\nActual Output:\n\n`%s`", expected, actual) } } func BenchmarkMarginString(b *testing.B) { b.RunParallel(func(pb *testing.PB) { b.ReportAllocs() b.ResetTimer() for pb.Next() { String("foobar", 10, 2) } }) } func TestWriter_Error(t *testing.T) { t.Parallel() f := &Writer{ iw: indent.NewWriter(2, nil), pw: padding.NewWriterPipe(fakeWriter{}, 10, nil), } if _, err := f.Write([]byte("foobar")); err != fakeErr { t.Error(err) } f.iw = indent.NewWriterPipe(fakeWriter{}, 2, nil) if _, err := f.Write([]byte("foobar")); err != fakeErr { t.Error(err) } if err := f.Close(); err != fakeErr { t.Error(err) } } var fakeErr = errors.New("fake error") type fakeWriter struct{} func (fakeWriter) Write(_ []byte) (int, error) { return 0, fakeErr } reflow-0.3.0/padding/000077500000000000000000000000001405043646000144205ustar00rootroot00000000000000reflow-0.3.0/padding/padding.go000066400000000000000000000054131405043646000163600ustar00rootroot00000000000000package padding import ( "bytes" "io" "strings" "github.com/mattn/go-runewidth" "github.com/muesli/reflow/ansi" ) type PaddingFunc func(w io.Writer) type Writer struct { Padding uint PadFunc PaddingFunc ansiWriter *ansi.Writer buf bytes.Buffer cache bytes.Buffer lineLen int ansi bool } func NewWriter(width uint, paddingFunc PaddingFunc) *Writer { w := &Writer{ Padding: width, PadFunc: paddingFunc, } w.ansiWriter = &ansi.Writer{ Forward: &w.buf, } return w } func NewWriterPipe(forward io.Writer, width uint, paddingFunc PaddingFunc) *Writer { return &Writer{ Padding: width, PadFunc: paddingFunc, ansiWriter: &ansi.Writer{ Forward: forward, }, } } // Bytes is shorthand for declaring a new default padding-writer instance, // used to immediately pad a byte slice. func Bytes(b []byte, width uint) []byte { f := NewWriter(width, nil) _, _ = f.Write(b) _ = f.Flush() return f.Bytes() } // String is shorthand for declaring a new default padding-writer instance, // used to immediately pad a string. func String(s string, width uint) string { return string(Bytes([]byte(s), width)) } // Write is used to write content to the padding buffer. func (w *Writer) Write(b []byte) (int, error) { for _, c := range string(b) { if c == '\x1B' { // ANSI escape sequence w.ansi = true } else if w.ansi { if (c >= 0x41 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a) { // ANSI sequence terminated w.ansi = false } } else { w.lineLen += runewidth.StringWidth(string(c)) if c == '\n' { // end of current line err := w.pad() if err != nil { return 0, err } w.ansiWriter.ResetAnsi() w.lineLen = 0 } } _, err := w.ansiWriter.Write([]byte(string(c))) if err != nil { return 0, err } } return len(b), nil } func (w *Writer) pad() error { if w.Padding > 0 && uint(w.lineLen) < w.Padding { if w.PadFunc != nil { for i := 0; i < int(w.Padding)-w.lineLen; i++ { w.PadFunc(w.ansiWriter) } } else { _, err := w.ansiWriter.Write([]byte(strings.Repeat(" ", int(w.Padding)-w.lineLen))) if err != nil { return err } } } return nil } // Close will finish the padding operation. func (w *Writer) Close() (err error) { return w.Flush() } // Bytes returns the padded result as a byte slice. func (w *Writer) Bytes() []byte { return w.cache.Bytes() } // String returns the padded result as a string. func (w *Writer) String() string { return w.cache.String() } // Flush will finish the padding operation. Always call it before trying to // retrieve the final result. func (w *Writer) Flush() (err error) { if w.lineLen != 0 { if err = w.pad(); err != nil { return } } w.cache.Reset() _, err = w.buf.WriteTo(&w.cache) w.lineLen = 0 w.ansi = false return } reflow-0.3.0/padding/padding_test.go000066400000000000000000000072461405043646000174250ustar00rootroot00000000000000package padding import ( "bytes" "errors" "io" "testing" "github.com/muesli/reflow/ansi" ) func TestPadding(t *testing.T) { t.Parallel() tt := []struct { Input string Expected string Padding uint }{ // No-op, should pass through: { "foobar", "foobar", 0, }, // Basic padding: { "foobar", "foobar ", 10, }, // Multi-line padding: { "foo\nbar", "foo \nbar ", 6, }, // Don't pad empty trailing lines: { "foo\nbar\n", "foo \nbar \n", 6, }, // ANSI sequence codes: { "\x1B[38;2;249;38;114mfoo", "\x1B[38;2;249;38;114mfoo ", 6, }, } for i, tc := range tt { f := NewWriter(tc.Padding, nil) _, err := f.Write([]byte(tc.Input)) if err != nil { t.Error(err) } if err := f.Close(); err != nil { t.Error(err) } if f.String() != tc.Expected { t.Errorf("Test %d, expected:\n\n`%s`\n\nActual Output:\n\n`%s`", i, tc.Expected, f.String()) } } } func TestPaddingWriter(t *testing.T) { t.Parallel() f := NewWriter(6, nil) _, err := f.Write([]byte("foo\n")) if err != nil { t.Error(err) } _, err = f.Write([]byte("bar")) if err != nil { t.Error(err) } if err := f.Close(); err != nil { t.Error(err) } exp := "foo \nbar " if f.String() != exp { t.Errorf("expected:\n\n`%s`\n\nActual Output:\n\n`%s`", exp, f.String()) } } func TestPaddingString(t *testing.T) { t.Parallel() actual := String("foobar", 10) expected := "foobar " if actual != expected { t.Errorf("expected:\n\n`%s`\n\nActual Output:\n\n`%s`", expected, actual) } } func BenchmarkPaddingString(b *testing.B) { b.RunParallel(func(pb *testing.PB) { b.ReportAllocs() b.ResetTimer() for pb.Next() { String("foobar", 10) } }) } func TestNewWriterPipe(t *testing.T) { t.Parallel() b := &bytes.Buffer{} f := NewWriterPipe(b, 10, nil) if _, err := f.Write([]byte("foobar")); err != nil { t.Error(err) } if err := f.Close(); err != nil { t.Error(err) } actual := b.String() expected := "foobar " if actual != expected { t.Errorf("expected:\n\n`%s`\n\nActual Output:\n\n`%s`", expected, actual) } } func TestWriter_pad(t *testing.T) { t.Parallel() f := NewWriter(4, func(w io.Writer) { _, _ = w.Write([]byte(".")) }) if err := f.pad(); err != nil { t.Error(err) } actual := f.buf.String() expected := "...." if actual != expected { t.Errorf("expected:\n\n`%s`\n\nActual Output:\n\n`%s`", expected, actual) } } func TestWriter_Flush(t *testing.T) { t.Parallel() f := NewWriter(6, nil) _, err := f.Write([]byte("foo")) if err != nil { t.Error(err) } if err := f.Flush(); err != nil { t.Error(err) } exp := "foo " if f.String() != exp { t.Errorf("expected:\n\n`%s`\n\nActual Output:\n\n`%s`", exp, f.String()) } _, err = f.Write([]byte("bar")) if err != nil { t.Error(err) } if err := f.Flush(); err != nil { t.Error(err) } exp = "bar " if f.String() != exp { t.Errorf("expected:\n\n`%s`\n\nActual Output:\n\n`%s`", exp, f.String()) } } func TestWriter_Close(t *testing.T) { t.Parallel() f := &Writer{ Padding: 6, lineLen: 1, ansiWriter: &ansi.Writer{Forward: fakeWriter{}}, } if err := f.Close(); err != fakeErr { t.Error(err) } } func TestWriter_Error(t *testing.T) { t.Parallel() f := &Writer{ Padding: 6, ansiWriter: &ansi.Writer{Forward: fakeWriter{}}, } if _, err := f.Write([]byte("foo\n")); err != fakeErr { t.Error(err) } if _, err := f.Write([]byte("\n")); err != fakeErr { t.Error(err) } if err := f.pad(); err != fakeErr { t.Error(err) } } var fakeErr = errors.New("fake error") type fakeWriter struct{} func (fakeWriter) Write(_ []byte) (int, error) { return 0, fakeErr } reflow-0.3.0/reflow.png000066400000000000000000000212561405043646000150240ustar00rootroot00000000000000PNG  IHDRv2oiCCPicc(u;KA?#bXHbARc&D_fy,$ 6 W?VUE W2{?̹̜@2o!(];G'BDh&Lf8t ^^sҦc@Caٮprյo w9--/J}~TU=`9 < [ȗ󨛴Ź=2#8H'N J-Jf|Scߢ-,9D-KWSjFtS<<Ȱ߽=-U=jxgź$9^k{й'uM߆ 辳4[e2x>_Aے:G0&Ot ;/;?}gܖӞ pHYsaa?i IDATx]iu/ͼmf_ hh1#Ќ  YȖHX ˩ʞTvJT~Tʉ9q9Owzyu^w=sOj?'vr@;99NNh''vrr@;99NNh''vrr@;99NNh'vrr@;9NNh''vrr@;99NNh''P"Q$TRN(L&Z$EoS$%9ǒ%~L,pXgGZ . c #`nmZt24 P4Ϫ܁Nf䆭vd03 Vg ,8Xr` Y\ƐCd;hWfFgRWuYE9Obb3ـmWVR\?nQi3 9+C38ɪ y. @ */ot5F`pXKOP kd3r?.n]v%4.XaYZVǂs<flPh U1 f92#,1yab!<֒W<./f#X`mMc KϨhY8a~4 o\֨GƱYH_!ty943$1V#Y(fQϢœx|:6eyU.,*k%pfK6_0/`FUcZ.rTH`ŌHqK/Qx0v[5;+z뭷$Tf3fL5{XXaP岼C6n8r䈬@f]+\dѣG>l|ЕR,rl<-c#udrtRP`w5\v `` uև~gϞog7oϤ)J[a=$¡688 w?㮮θ//t䀮+"Qf2\tTxƍs 7m8yroKK&&GFF0`z{{̙3~Dp908o[VdŊ.\ذa}gg6'۶}۷%ukJD @ˢ/U!0ft7ސ vbcHrKv@]f9 )f 7JZ1-FKW^yEٳ$]v!er)bL;kf+6hG9|«:^dXMΝ;%[8=Av}36;S ٨M԰v%[n5<7fmhh)4ۥK*h# HEׯc9f*͠Ӳ-Mm,\rEwr{UH`pġϙ;$;0hl@^ήl`$ rw},HAaWHCO?\Et1mel84oHaرc駟BK6ᆕ %a%ZVjc憆~*H!'o||jK0mzf- Z͊>2HY(ǏϢ24[YXFEmšAYoVOIܾ}rVQI=;Q8ٲe3:|3?/ԩS:eY=U̓kB@űyumd|W.-2m+8t\0=Lө&!6|fnZ;-lDKTp}]jfmm`0ie Kq{nv\^N^)׮]؉]v3!81&o^q_dAlbb\`PٳW=B׻-&.SL-ƞo3::u>}ZN{]p쑓Sy 6Bsָ۝f zjb 4ŋtYWWNm5݀pfAFفm Yc\ӧOIΧzJɓ{y>+$--Mr|}?^sƦ5eYN-, C--f ڵkTZذqacǎI~GGGW Ef2Zp׿^__䥡|g>Zyg4߇Mc]^z}[8ȱ2ꎸwe3K:y/ jbC`~iX~<)cojB@nJn*3{fKΜ9ׯ44EamUxai؂Idc/JWk$$ cm#Su(wnVXXb/Po9L(CCv2MNvw6"5U{/d00ǿcYəhOŷ/C(iGTR.( L G7xa^J?b\XgFGavp"41Yh%7ۮr6b1{Qmqw[f=g>Ab'Fb8ټ0!ܒ!ʍ c>°*%[i 6e߱bg~hqUe2p{~U_㚏By]3Y65̘1gʟaUc ૺ^MTB*;EjV!Wf5y#:F2L_Q=;gSL-Z?~FxK9|Xkboy܍xlL&=o=ƒccc˖ גE6sOz7N-ZhѢ/5aM唡UGg5ttΝWhAy|ڡd ӮP{Gӭ9V./jv:]/'MJ" zgdJIQh6ԣ Qͩ騟azp7mLMO\ɂ炘 V-Q8U&Vtau( /EetL[(C'kI|S9Q4" R=AC"FJ]; ٵkAs1 K(XAӝtg rqN87f#'v߱o|g?{㮭k*慷:7-D%hHOеx#&Yݗn<ߺG{~T:D4 Մbٺ*L.[uѝڄ!AM*j9=۷o߸q .FO:X҆4T:kfZ_GsdO_R'Im\II5%3 rWj'WΥ2RyphP37 ՓDzV͚5\Nl٬o޼۽{V=lnii. 5s :ztk6MVB٪rdbYm"M=5YIH]m_7O՜R1.+f!-ՓVM?ԙiqIACuDcCBJj_2qB% l LgRlZVGHX[g̙>rH{{dnja 㒥KPwyG`i\'rC^NК/OR>&~ػ7-gT*ݏHo<x ]k_/vlԪ/wmS\*(ownݕI&ЮP]7?$, .ɤpϪx~QPʹtR٣_~Y1Ե g]r9 -]l, P}*$D $hvH~) &qI8G$M`v&A2޽r]F 4 7|#v5m+WXrӧiٚڌC6m#M9}bڐSN!g}PTWCΦ%u]Ac3YFL2)K(975L zN%Uƹc&?њ·jLSq:#pbIum^׶O:\O)A> >+. 3v@}QѬF1Lufj^^QfF;w$w% YouT)an][VTVN:2zk{ÌQ#(Ic{$񡆙r}j)-oX[EDz=ĥDPv3X|"of(r5Vrlii[޻D"զLaS쥝r@jqqpQX$[wM((.L4qBwgZGeI[rThyq$Oi S{~lA 2 &2.{jC֞˗߯-| X~Ss##zʎY2:yhH:󪄆]\I3tȕgnm/ͩmUM^T-.l&ɭoG֓GddF *6EHzuhwTgnԬ=7'UOG>[l9k^na;'{=Uҳ#x39 :bWnn:t;r/|<wj+pR'7ޭ<7OeLrnG[;knH9@[[~ŋ*KhYIqrh-"qE:o=K(< fϞta7Ztyx!q?m*&zv(#AC= +6-G{ؘEPVz_5d\G4ϟ>Kk/=nW ѥCNpK-|S/=z䥗^RJTwtٳ*]9TxLh$%qˤVڏ.9'uX#W2L Ji?ѥP- يX*Y`!ކ6YLg r,*Rk3 8B&]-GBѐ>3Ep)t|/:G=!MEfFsQ[HǠ-Ɔ~ $!`!*!0ÐÃңN*W XcAYV`Q2djK˘f-VUq[%tF?X// .0.‰~i6c!wg^:-rDjM&4hЦA(t2k&^W!O(ؙȩo1̰Qq6) BH?fQb|- Z8I& N2CR]^RbL2H: ( ?aN]МHaŜP[q~B<$R֤qRЙHLARCWgUHhvrr@;99NNh''vrr@;99NNh'vr@#pr@;99NNh'vrr@;9NNh''vrr@;99Nh''vM įK:IENDB`reflow-0.3.0/truncate/000077500000000000000000000000001405043646000146375ustar00rootroot00000000000000reflow-0.3.0/truncate/truncate.go000066400000000000000000000051621405043646000170170ustar00rootroot00000000000000package truncate import ( "bytes" "io" "github.com/mattn/go-runewidth" "github.com/muesli/reflow/ansi" ) type Writer struct { width uint tail string ansiWriter *ansi.Writer buf bytes.Buffer ansi bool } func NewWriter(width uint, tail string) *Writer { w := &Writer{ width: width, tail: tail, } w.ansiWriter = &ansi.Writer{ Forward: &w.buf, } return w } func NewWriterPipe(forward io.Writer, width uint, tail string) *Writer { return &Writer{ width: width, tail: tail, ansiWriter: &ansi.Writer{ Forward: forward, }, } } // Bytes is shorthand for declaring a new default truncate-writer instance, // used to immediately truncate a byte slice. func Bytes(b []byte, width uint) []byte { return BytesWithTail(b, width, []byte("")) } // Bytes is shorthand for declaring a new default truncate-writer instance, // used to immediately truncate a byte slice. A tail is then added to the // end of the byte slice. func BytesWithTail(b []byte, width uint, tail []byte) []byte { f := NewWriter(width, string(tail)) _, _ = f.Write(b) return f.Bytes() } // String is shorthand for declaring a new default truncate-writer instance, // used to immediately truncate a string. func String(s string, width uint) string { return StringWithTail(s, width, "") } // StringWithTail is shorthand for declaring a new default truncate-writer instance, // used to immediately truncate a string. A tail is then added to the end of the // string. func StringWithTail(s string, width uint, tail string) string { return string(BytesWithTail([]byte(s), width, []byte(tail))) } // Write truncates content at the given printable cell width, leaving any // ansi sequences intact. func (w *Writer) Write(b []byte) (int, error) { tw := ansi.PrintableRuneWidth(w.tail) if w.width < uint(tw) { return w.buf.WriteString(w.tail) } w.width -= uint(tw) var curWidth uint for _, c := range string(b) { if c == ansi.Marker { // ANSI escape sequence w.ansi = true } else if w.ansi { if ansi.IsTerminator(c) { // ANSI sequence terminated w.ansi = false } } else { curWidth += uint(runewidth.RuneWidth(c)) } if curWidth > w.width { n, err := w.buf.WriteString(w.tail) if w.ansiWriter.LastSequence() != "" { w.ansiWriter.ResetAnsi() } return n, err } _, err := w.ansiWriter.Write([]byte(string(c))) if err != nil { return 0, err } } return len(b), nil } // Bytes returns the truncated result as a byte slice. func (w *Writer) Bytes() []byte { return w.buf.Bytes() } // String returns the truncated result as a string. func (w *Writer) String() string { return w.buf.String() } reflow-0.3.0/truncate/truncate_test.go000066400000000000000000000055221405043646000200560ustar00rootroot00000000000000package truncate import ( "bytes" "errors" "testing" "github.com/muesli/reflow/ansi" ) func TestTruncate(t *testing.T) { t.Parallel() tt := []struct { width uint tail string in string expected string }{ // No-op, should pass through: { 10, "", "foo", "foo", }, // Basic truncate: { 3, "", "foobar", "foo", }, // Truncate with tail: { 4, ".", "foobar", "foo.", }, // Same width: { 3, "", "foo", "foo", }, // Tail is longer than width: { 2, "...", "foo", "...", }, // Spaces only: { 2, "…", " ", " …", }, // Double-width runes: { 2, "", "你好", "你", }, // Double-width rune is dropped if it is too wide: { 1, "", "你", "", }, // ANSI sequence codes and double-width characters: { 3, "", "\x1B[38;2;249;38;114m你好\x1B[0m", "\x1B[38;2;249;38;114m你\x1B[0m", }, // Reset styling sequence is added after truncate: { 1, "", "\x1B[7m--", "\x1B[7m-\x1B[0m", }, // Reset styling sequence not added if operation is a noop: { 2, "", "\x1B[7m--", "\x1B[7m--", }, // Tail is printed before reset sequence: { 3, "…", "\x1B[38;5;219mHiya!", "\x1B[38;5;219mHi…\x1B[0m", }, } for i, tc := range tt { f := NewWriter(tc.width, tc.tail) _, err := f.Write([]byte(tc.in)) if err != nil { t.Error(err) } if f.String() != tc.expected { t.Errorf("Test %d, expected:\n\n`%s`\n\nActual Output:\n\n`%s`", i, tc.expected, f.String()) } } } func TestTruncateString(t *testing.T) { t.Parallel() actual := String("foobar", 3) expected := "foo" if actual != expected { t.Errorf("expected:\n\n`%s`\n\nActual Output:\n\n`%s`", expected, actual) } } func BenchmarkTruncateString(b *testing.B) { b.RunParallel(func(pb *testing.PB) { b.ReportAllocs() b.ResetTimer() for pb.Next() { String("foo", 2) } }) } func TestTruncateBytes(t *testing.T) { t.Parallel() actual := Bytes([]byte("foobar"), 3) expected := []byte("foo") if !bytes.Equal(actual, expected) { t.Errorf("expected:\n\n`%s`\n\nActual Output:\n\n`%s`", expected, actual) } } func TestNewWriterPipe(t *testing.T) { t.Parallel() b := &bytes.Buffer{} f := NewWriterPipe(b, 2, "") if _, err := f.Write([]byte("foo")); err != nil { t.Error(err) } actual := b.String() expected := "fo" if actual != expected { t.Errorf("expected:\n\n`%s`\n\nActual Output:\n\n`%s`", expected, actual) } } func TestWriter_Error(t *testing.T) { t.Parallel() f := &Writer{ width: 2, ansiWriter: &ansi.Writer{Forward: fakeWriter{}}, } if _, err := f.Write([]byte("foo")); err != fakeErr { t.Error(err) } } var fakeErr = errors.New("fake error") type fakeWriter struct{} func (fakeWriter) Write(_ []byte) (int, error) { return 0, fakeErr } reflow-0.3.0/wordwrap/000077500000000000000000000000001405043646000146575ustar00rootroot00000000000000reflow-0.3.0/wordwrap/wordwrap.go000066400000000000000000000071271405043646000170620ustar00rootroot00000000000000package wordwrap import ( "bytes" "strings" "unicode" "github.com/muesli/reflow/ansi" ) var ( defaultBreakpoints = []rune{'-'} defaultNewline = []rune{'\n'} ) // WordWrap contains settings and state for customisable text reflowing with // support for ANSI escape sequences. This means you can style your terminal // output without affecting the word wrapping algorithm. type WordWrap struct { Limit int Breakpoints []rune Newline []rune KeepNewlines bool buf bytes.Buffer space bytes.Buffer word ansi.Buffer lineLen int ansi bool } // NewWriter returns a new instance of a word-wrapping writer, initialized with // default settings. func NewWriter(limit int) *WordWrap { return &WordWrap{ Limit: limit, Breakpoints: defaultBreakpoints, Newline: defaultNewline, KeepNewlines: true, } } // Bytes is shorthand for declaring a new default WordWrap instance, // used to immediately word-wrap a byte slice. func Bytes(b []byte, limit int) []byte { f := NewWriter(limit) _, _ = f.Write(b) _ = f.Close() return f.Bytes() } // String is shorthand for declaring a new default WordWrap instance, // used to immediately word-wrap a string. func String(s string, limit int) string { return string(Bytes([]byte(s), limit)) } func (w *WordWrap) addSpace() { w.lineLen += w.space.Len() _, _ = w.buf.Write(w.space.Bytes()) w.space.Reset() } func (w *WordWrap) addWord() { if w.word.Len() > 0 { w.addSpace() w.lineLen += w.word.PrintableRuneWidth() _, _ = w.buf.Write(w.word.Bytes()) w.word.Reset() } } func (w *WordWrap) addNewLine() { _, _ = w.buf.WriteRune('\n') w.lineLen = 0 w.space.Reset() } func inGroup(a []rune, c rune) bool { for _, v := range a { if v == c { return true } } return false } // Write is used to write more content to the word-wrap buffer. func (w *WordWrap) Write(b []byte) (int, error) { if w.Limit == 0 { return w.buf.Write(b) } s := string(b) if !w.KeepNewlines { s = strings.Replace(strings.TrimSpace(s), "\n", " ", -1) } for _, c := range s { if c == '\x1B' { // ANSI escape sequence _, _ = w.word.WriteRune(c) w.ansi = true } else if w.ansi { _, _ = w.word.WriteRune(c) if (c >= 0x40 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a) { // ANSI sequence terminated w.ansi = false } } else if inGroup(w.Newline, c) { // end of current line // see if we can add the content of the space buffer to the current line if w.word.Len() == 0 { if w.lineLen+w.space.Len() > w.Limit { w.lineLen = 0 } else { // preserve whitespace _, _ = w.buf.Write(w.space.Bytes()) } w.space.Reset() } w.addWord() w.addNewLine() } else if unicode.IsSpace(c) { // end of current word w.addWord() _, _ = w.space.WriteRune(c) } else if inGroup(w.Breakpoints, c) { // valid breakpoint w.addSpace() w.addWord() _, _ = w.buf.WriteRune(c) } else { // any other character _, _ = w.word.WriteRune(c) // add a line break if the current word would exceed the line's // character limit if w.lineLen+w.space.Len()+w.word.PrintableRuneWidth() > w.Limit && w.word.PrintableRuneWidth() < w.Limit { w.addNewLine() } } } return len(b), nil } // Close will finish the word-wrap operation. Always call it before trying to // retrieve the final result. func (w *WordWrap) Close() error { w.addWord() return nil } // Bytes returns the word-wrapped result as a byte slice. func (w *WordWrap) Bytes() []byte { return w.buf.Bytes() } // String returns the word-wrapped result as a string. func (w *WordWrap) String() string { return w.buf.String() } reflow-0.3.0/wordwrap/wordwrap_test.go000066400000000000000000000061411405043646000201140ustar00rootroot00000000000000package wordwrap import ( "testing" ) func TestWordWrap(t *testing.T) { tt := []struct { Input string Expected string Limit int KeepNewlines bool }{ // No-op, should pass through, including trailing whitespace: { "foobar\n ", "foobar\n ", 0, true, }, // Nothing to wrap here, should pass through: { "foo", "foo", 4, true, }, // A single word that is too long passes through. // We do not break long words: { "foobarfoo", "foobarfoo", 4, true, }, // Lines are broken at whitespace: { "foo bar foo", "foo\nbar\nfoo", 4, true, }, // A hyphen is a valid breakpoint: { "foo-foobar", "foo-\nfoobar", 4, true, }, // Space buffer needs to be emptied before breakpoints: { "foo --bar", "foo --bar", 9, true, }, // Lines are broken at whitespace, even if words // are too long. We do not break words: { "foo bars foobars", "foo\nbars\nfoobars", 4, true, }, // A word that would run beyond the limit is wrapped: { "foo bar", "foo\nbar", 5, true, }, // Whitespace that trails a line and fits the width // passes through, as does whitespace prefixing an // explicit line break. A tab counts as one character: { "foo\nb\t a\n bar", "foo\nb\t a\n bar", 4, true, }, // Trailing whitespace is removed if it doesn't fit the width. // Runs of whitespace on which a line is broken are removed: { "foo \nb ar ", "foo\nb\nar", 4, true, }, // An explicit line break at the end of the input is preserved: { "foo bar foo\n", "foo\nbar\nfoo\n", 4, true, }, // Explicit break are always preserved: { "\nfoo bar\n\n\nfoo\n", "\nfoo\nbar\n\n\nfoo\n", 4, true, }, // Unless we ask them to be ignored: { "\nfoo bar\n\n\nfoo\n", "foo\nbar\nfoo", 4, false, }, // Complete example: { " This is a list: \n\n\t* foo\n\t* bar\n\n\n\t* foo \nbar ", " This\nis a\nlist: \n\n\t* foo\n\t* bar\n\n\n\t* foo\nbar", 6, true, }, // ANSI sequence codes don't affect length calculation: { "\x1B[38;2;249;38;114mfoo\x1B[0m\x1B[38;2;248;248;242m \x1B[0m\x1B[38;2;230;219;116mbar\x1B[0m", "\x1B[38;2;249;38;114mfoo\x1B[0m\x1B[38;2;248;248;242m \x1B[0m\x1B[38;2;230;219;116mbar\x1B[0m", 7, true, }, // ANSI control codes don't get wrapped: { "\x1B[38;2;249;38;114m(\x1B[0m\x1B[38;2;248;248;242mjust another test\x1B[38;2;249;38;114m)\x1B[0m", "\x1B[38;2;249;38;114m(\x1B[0m\x1B[38;2;248;248;242mjust\nanother\ntest\x1B[38;2;249;38;114m)\x1B[0m", 3, true, }, } for i, tc := range tt { f := NewWriter(tc.Limit) f.KeepNewlines = tc.KeepNewlines _, err := f.Write([]byte(tc.Input)) if err != nil { t.Error(err) } f.Close() if f.String() != tc.Expected { t.Errorf("Test %d, expected:\n\n`%s`\n\nActual Output:\n\n`%s`", i, tc.Expected, f.String()) } } } func TestWordWrapString(t *testing.T) { actual := String("foo bar", 3) expected := "foo\nbar" if actual != expected { t.Errorf("expected:\n\n`%s`\n\nActual Output:\n\n`%s`", expected, actual) } } reflow-0.3.0/wrap/000077500000000000000000000000001405043646000137635ustar00rootroot00000000000000reflow-0.3.0/wrap/wrap.go000066400000000000000000000051761405043646000152740ustar00rootroot00000000000000package wrap import ( "bytes" "strings" "unicode" "github.com/mattn/go-runewidth" "github.com/muesli/reflow/ansi" ) var ( defaultNewline = []rune{'\n'} defaultTabWidth = 4 ) type Wrap struct { Limit int Newline []rune KeepNewlines bool PreserveSpace bool TabWidth int buf *bytes.Buffer lineLen int ansi bool forcefulNewline bool } // NewWriter returns a new instance of a wrapping writer, initialized with // default settings. func NewWriter(limit int) *Wrap { return &Wrap{ Limit: limit, Newline: defaultNewline, KeepNewlines: true, // Keep whitespaces following a forceful line break. If disabled, // leading whitespaces in a line are only kept if the line break // was not forceful, meaning a line break that was already present // in the input PreserveSpace: false, TabWidth: defaultTabWidth, buf: &bytes.Buffer{}, } } // Bytes is shorthand for declaring a new default Wrap instance, // used to immediately wrap a byte slice. func Bytes(b []byte, limit int) []byte { f := NewWriter(limit) _, _ = f.Write(b) return f.buf.Bytes() } func (w *Wrap) addNewLine() { _, _ = w.buf.WriteRune('\n') w.lineLen = 0 } // String is shorthand for declaring a new default Wrap instance, // used to immediately wrap a string. func String(s string, limit int) string { return string(Bytes([]byte(s), limit)) } func (w *Wrap) Write(b []byte) (int, error) { s := strings.Replace(string(b), "\t", strings.Repeat(" ", w.TabWidth), -1) if !w.KeepNewlines { s = strings.Replace(s, "\n", "", -1) } width := ansi.PrintableRuneWidth(s) if w.Limit <= 0 || w.lineLen+width <= w.Limit { w.lineLen += width return w.buf.Write(b) } for _, c := range s { if c == ansi.Marker { w.ansi = true } else if w.ansi { if ansi.IsTerminator(c) { w.ansi = false } } else if inGroup(w.Newline, c) { w.addNewLine() w.forcefulNewline = false continue } else { width := runewidth.RuneWidth(c) if w.lineLen+width > w.Limit { w.addNewLine() w.forcefulNewline = true } if w.lineLen == 0 { if w.forcefulNewline && !w.PreserveSpace && unicode.IsSpace(c) { continue } } else { w.forcefulNewline = false } w.lineLen += width } _, _ = w.buf.WriteRune(c) } return len(b), nil } // Bytes returns the wrapped result as a byte slice. func (w *Wrap) Bytes() []byte { return w.buf.Bytes() } // String returns the wrapped result as a string. func (w *Wrap) String() string { return w.buf.String() } func inGroup(a []rune, c rune) bool { for _, v := range a { if v == c { return true } } return false } reflow-0.3.0/wrap/wrap_test.go000066400000000000000000000070631405043646000163300ustar00rootroot00000000000000package wrap import ( "testing" ) func TestWrap(t *testing.T) { tt := []struct { Input string Expected string Limit int KeepNewlines bool PreserveSpace bool TabWidth int }{ // No-op, should pass through, including trailing whitespace: { Input: "foobar\n ", Expected: "foobar\n ", Limit: 0, KeepNewlines: true, PreserveSpace: false, TabWidth: 0, }, // Nothing to wrap here, should pass through: { Input: "foo", Expected: "foo", Limit: 4, KeepNewlines: true, PreserveSpace: false, TabWidth: 0, }, // In contrast to wordwrap we break a long word to obey the given limit { Input: "foobarfoo", Expected: "foob\narfo\no", Limit: 4, KeepNewlines: true, PreserveSpace: false, TabWidth: 0, }, // Newlines in the input are respected if desired { Input: "f\no\nobar", Expected: "f\no\noba\nr", Limit: 3, KeepNewlines: true, PreserveSpace: false, TabWidth: 0, }, // Newlines in the input can be ignored if desired { Input: "f\no\nobar", Expected: "foo\nbar", Limit: 3, KeepNewlines: false, PreserveSpace: false, TabWidth: 0, }, // Leading whitespaces after forceful line break can be preserved if desired { Input: "foo bar\n baz", Expected: "foo\n ba\nr\n b\naz", Limit: 3, KeepNewlines: true, PreserveSpace: true, TabWidth: 0, }, // Leading whitespaces after forceful line break can be removed if desired { Input: "foo bar\n baz", Expected: "foo\nbar\n b\naz", Limit: 3, KeepNewlines: true, PreserveSpace: false, TabWidth: 0, }, // Tabs are broken up according to the configured TabWidth { Input: "foo\tbar", Expected: "foo \n ba\nr", Limit: 4, KeepNewlines: true, PreserveSpace: true, TabWidth: 3, }, // Remaining width of wrapped tab is ignored when space is not preserved { Input: "foo\tbar", Expected: "foo \nbar", Limit: 4, KeepNewlines: true, PreserveSpace: false, TabWidth: 3, }, // ANSI sequence codes don't affect length calculation: { Input: "\x1B[38;2;249;38;114mfoo\x1B[0m\x1B[38;2;248;248;242m \x1B[0m\x1B[38;2;230;219;116mbar\x1B[0m", Expected: "\x1B[38;2;249;38;114mfoo\x1B[0m\x1B[38;2;248;248;242m \x1B[0m\x1B[38;2;230;219;116mbar\x1B[0m", Limit: 7, KeepNewlines: true, PreserveSpace: false, TabWidth: 0, }, // ANSI control codes don't get wrapped: { Input: "\x1B[38;2;249;38;114m(\x1B[0m\x1B[38;2;248;248;242mjust another test\x1B[38;2;249;38;114m)\x1B[0m", Expected: "\x1B[38;2;249;38;114m(\x1B[0m\x1B[38;2;248;248;242mju\nst \nano\nthe\nr t\nest\x1B[38;2;249;38;114m\n)\x1B[0m", Limit: 3, KeepNewlines: true, PreserveSpace: false, TabWidth: 0, }, } for i, tc := range tt { f := NewWriter(tc.Limit) f.KeepNewlines = tc.KeepNewlines f.PreserveSpace = tc.PreserveSpace f.TabWidth = tc.TabWidth _, err := f.Write([]byte(tc.Input)) if err != nil { t.Error(err) } if f.String() != tc.Expected { t.Errorf("Test %d, expected:\n\n`%s`\n\nActual Output:\n\n`%s`", i, tc.Expected, f.String()) } } } func TestWrapString(t *testing.T) { actual := String("foo bar", 3) expected := "foo\nbar" if actual != expected { t.Errorf("expected:\n\n`%s`\n\nActual Output:\n\n`%s`", expected, actual) } }