Skip to content

Commit 81e21bd

Browse files
author
Kilian Drechsler
committed
added PreserveSpaces and TabReplace to struct
for better managing char-preserving and hardwrap
1 parent 5dded64 commit 81e21bd

File tree

2 files changed

+164
-36
lines changed

2 files changed

+164
-36
lines changed

wordwrap/wordwrap.go

+60-18
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,23 @@ var (
1818
// support for ANSI escape sequences. This means you can style your terminal
1919
// output without affecting the word wrapping algorithm.
2020
type WordWrap struct {
21-
Limit int
22-
Breakpoints []rune
23-
Newline []rune
24-
KeepNewlines bool
25-
HardBreak bool
26-
27-
buf bytes.Buffer // processed and in line accepted bytes
21+
Limit int
22+
Breakpoints []rune
23+
Newline []rune
24+
KeepNewlines bool
25+
HardWrap bool
26+
TabReplace string // since tabs can have differrent lenghts, replace them with this when hardwrap is enabled
27+
PreserveSpaces bool
28+
29+
buf bytes.Buffer // processed and, in line, accepted bytes
2830
space bytes.Buffer // pending continues spaces bytes
2931
word ansi.Buffer // pending continues word bytes
3032

31-
hardWriter ansi.Writer
33+
lineLen int // the visible lenght of the line not accorat for tabs
34+
ansi bool
3235

33-
wroteBegin bool
34-
lineLen int
35-
ansi bool
36-
lastAnsi bytes.Buffer
36+
wroteBegin bool // mark is since the last newline something has writen to the buffer (for ansi restart)
37+
lastAnsi bytes.Buffer // hold last active ansi sequence
3738
}
3839

3940
// NewWriter returns a new instance of a word-wrapping writer, initialized with
@@ -63,10 +64,38 @@ func String(s string, limit int) string {
6364
return string(Bytes([]byte(s), limit))
6465
}
6566

67+
// HardWrap is a shorthand for declaring a new hardwraping WordWrap instance,
68+
// since varibale lenght characters can not be hard wraped to a fixed lenght,
69+
// tabs will be replaced by TabReplace, use according amount of spaces.
70+
func HardWrap(s string, limit int, tabReplace string) string {
71+
f := NewWriter(limit)
72+
f.HardWrap = true
73+
f.TabReplace = tabReplace
74+
_, _ = f.Write([]byte(s))
75+
f.Close()
76+
77+
return f.String()
78+
}
79+
6680
// addes pending spaces to the buf(fer) ... and resets the space buffer
6781
func (w *WordWrap) addSpace() {
68-
w.lineLen += w.space.Len()
69-
_, _ = w.buf.Write(w.space.Bytes())
82+
if w.space.Len() <= w.Limit-w.lineLen {
83+
w.lineLen += w.space.Len()
84+
w.buf.Write(w.space.Bytes())
85+
} else {
86+
lenght := w.space.Len()
87+
first := w.Limit - w.lineLen
88+
w.buf.WriteString(strings.Repeat(" ", first))
89+
lenght -= first
90+
for lenght >= w.Limit {
91+
w.buf.WriteString("\n" + strings.Repeat(" ", w.Limit))
92+
lenght -= w.Limit
93+
}
94+
if lenght > 0 {
95+
w.buf.WriteString("\n" + strings.Repeat(" ", lenght))
96+
}
97+
w.lineLen = lenght
98+
}
7099
w.space.Reset()
71100
}
72101

@@ -80,6 +109,9 @@ func (w *WordWrap) addWord() {
80109
}
81110

82111
func (w *WordWrap) addNewLine() {
112+
if w.PreserveSpaces {
113+
w.addSpace()
114+
}
83115
if w.lastAnsi.Len() != 0 {
84116
// end ansi befor linebreak
85117
w.buf.WriteString("\x1b[0m")
@@ -110,6 +142,10 @@ func (w *WordWrap) Write(b []byte) (int, error) {
110142
s = strings.Replace(strings.TrimSpace(s), "\n", " ", -1)
111143
}
112144

145+
if w.HardWrap {
146+
s = strings.ReplaceAll(s, "\t", w.TabReplace)
147+
}
148+
113149
for _, c := range s {
114150
// Restart Ansi after line break if there is more text
115151
if !w.wroteBegin && !w.ansi && w.lastAnsi.Len() != 0 {
@@ -156,11 +192,10 @@ func (w *WordWrap) Write(b []byte) (int, error) {
156192
w.addSpace()
157193
w.addWord()
158194
w.buf.WriteRune(c)
159-
} else if w.HardBreak && w.lineLen+runewidth.RuneWidth(c) > w.Limit {
160-
// Word excends the limite -> break and begin new line/word
195+
} else if w.HardWrap && w.lineLen+w.word.PrintableRuneWidth()+runewidth.RuneWidth(c)+w.space.Len() == w.Limit {
196+
// Word is at the limite -> begin new word
197+
w.word.WriteRune(c)
161198
w.addWord()
162-
w.addNewLine()
163-
w.buf.WriteRune(c)
164199
} else {
165200
// any other character
166201
_, _ = w.word.WriteRune(c)
@@ -180,7 +215,14 @@ func (w *WordWrap) Write(b []byte) (int, error) {
180215
// Close will finish the word-wrap operation. Always call it before trying to
181216
// retrieve the final result.
182217
func (w *WordWrap) Close() error {
218+
if w.HardWrap && w.word.PrintableRuneWidth()+w.lineLen > w.Limit {
219+
w.addNewLine()
220+
}
221+
if w.PreserveSpaces {
222+
w.addSpace()
223+
}
183224
w.addWord()
225+
184226
return nil
185227
}
186228

wordwrap/wordwrap_test.go

+104-18
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,20 @@ func TestWordWrap(t *testing.T) {
1010
Expected string
1111
Limit int
1212
KeepNewlines bool
13-
HardWrap bool
1413
}{
1514
// No-op, should pass through, including trailing whitespace:
1615
{
1716
"foobar\n ",
1817
"foobar\n ",
1918
0,
2019
true,
21-
false,
2220
},
2321
// Nothing to wrap here, should pass through:
2422
{
2523
"foo",
2624
"foo",
2725
4,
2826
true,
29-
false,
3027
},
3128
// A single word that is too long passes through.
3229
// We do not break long words:
@@ -35,31 +32,27 @@ func TestWordWrap(t *testing.T) {
3532
"foobarfoo",
3633
4,
3734
true,
38-
false,
3935
},
4036
// Lines are broken at whitespace:
4137
{
4238
"foo bar foo",
4339
"foo\nbar\nfoo",
4440
4,
4541
true,
46-
false,
4742
},
4843
// A hyphen is a valid breakpoint:
4944
{
5045
"foo-foobar",
5146
"foo-\nfoobar",
5247
4,
5348
true,
54-
false,
5549
},
5650
// Space buffer needs to be emptied before breakpoints:
5751
{
5852
"foo --bar",
5953
"foo --bar",
6054
9,
6155
true,
62-
false,
6356
},
6457
// Lines are broken at whitespace, even if words
6558
// are too long. We do not break words:
@@ -68,15 +61,13 @@ func TestWordWrap(t *testing.T) {
6861
"foo\nbars\nfoobars",
6962
4,
7063
true,
71-
false,
7264
},
7365
// A word that would run beyond the limit is wrapped:
7466
{
7567
"foo bar",
7668
"foo\nbar",
7769
5,
7870
true,
79-
false,
8071
},
8172
// Whitespace that trails a line and fits the width
8273
// passes through, as does whitespace prefixing an
@@ -86,7 +77,6 @@ func TestWordWrap(t *testing.T) {
8677
"foo\nb\t a\n bar",
8778
4,
8879
true,
89-
false,
9080
},
9181
// Trailing whitespace is removed if it doesn't fit the width.
9282
// Runs of whitespace on which a line is broken are removed:
@@ -95,61 +85,53 @@ func TestWordWrap(t *testing.T) {
9585
"foo\nb\nar",
9686
4,
9787
true,
98-
false,
9988
},
10089
// An explicit line break at the end of the input is preserved:
10190
{
10291
"foo bar foo\n",
10392
"foo\nbar\nfoo\n",
10493
4,
10594
true,
106-
false,
10795
},
10896
// Explicit break are always preserved:
10997
{
11098
"\nfoo bar\n\n\nfoo\n",
11199
"\nfoo\nbar\n\n\nfoo\n",
112100
4,
113101
true,
114-
false,
115102
},
116103
// Unless we ask them to be ignored:
117104
{
118105
"\nfoo bar\n\n\nfoo\n",
119106
"foo\nbar\nfoo",
120107
4,
121108
false,
122-
false,
123109
},
124110
// Complete example:
125111
{
126112
" This is a list: \n\n\t* foo\n\t* bar\n\n\n\t* foo \nbar ",
127113
" This\nis a\nlist: \n\n\t* foo\n\t* bar\n\n\n\t* foo\nbar",
128114
6,
129115
true,
130-
false,
131116
},
132117
// ANSI sequence codes don't affect length calculation:
133118
{
134119
"\x1B[38;2;249;38;114mfoo\x1B[0m\x1B[38;2;248;248;242m \x1B[0m\x1B[38;2;230;219;116mbar\x1B[0m",
135120
"\x1B[38;2;249;38;114mfoo\x1B[0m\x1B[38;2;248;248;242m \x1B[0m\x1B[38;2;230;219;116mbar\x1B[0m",
136121
7,
137122
true,
138-
false,
139123
},
140124
// ANSI control codes don't get wrapped, but get finished and again started at each line break:
141125
{
142126
"\x1B[38;2;249;38;114m(\x1B[0m\x1B[38;2;248;248;242mjust another test\x1B[38;2;249;38;114m)\x1B[0m",
143127
"\x1B[38;2;249;38;114m(\x1B[0m\x1B[38;2;248;248;242mjust\x1B[0m\n\x1B[38;2;248;248;242manother\x1B[0m\n\x1B[38;2;248;248;242mtest\x1B[38;2;249;38;114m)\x1B[0m",
144128
3,
145129
true,
146-
false,
147130
},
148131
}
149132

150133
for i, tc := range tt {
151134
f := NewWriter(tc.Limit)
152-
f.HardBreak = tc.HardWrap
153135
f.KeepNewlines = tc.KeepNewlines
154136

155137
_, err := f.Write([]byte(tc.Input))
@@ -171,3 +153,107 @@ func TestWordWrapString(t *testing.T) {
171153
t.Errorf("expected:\n\n`%s`\n\nActual Output:\n\n`%s`", expected, actual)
172154
}
173155
}
156+
157+
func TestHardWrap(t *testing.T) {
158+
tt := []struct {
159+
Input string
160+
Expected string
161+
Limit int
162+
KeepNewlines bool
163+
PreserveSpaces bool
164+
TabReplace string
165+
}{
166+
// hardwrap wraps at limit and does not add newline if there is not more text
167+
{
168+
"foobarfoobar",
169+
"foo\nbar\nfoo\nbar",
170+
3,
171+
true,
172+
true,
173+
"",
174+
},
175+
// With no TabReplace string tabs get ignored
176+
{
177+
"\tfoo\tbar\tfoo\tbar",
178+
"foo\nbar\nfoo\nbar",
179+
3,
180+
true,
181+
true,
182+
"",
183+
},
184+
// limit of zero gets ignored
185+
{
186+
"foobar",
187+
"foobar",
188+
0,
189+
true,
190+
true,
191+
"",
192+
},
193+
// ANSI gets ended and restarted at each linebreak, so that the ansi is self contained within a line.
194+
{
195+
"\x1B[38;2;249;38;114m(\x1B[0m\x1B[38;2;248;248;242mjust an\nother test\x1B[38;2;249;38;114m)\x1B[0m",
196+
`(ju
197+
st 
198+
an
199+
oth
200+
er 
201+
tes
202+
t)`,
203+
3,
204+
true,
205+
true,
206+
"",
207+
},
208+
// if requested spaces are preserved as are Explicit linebreaks:
209+
{
210+
"\nfoo bar\n\n\nfoo\n",
211+
"\nfoo \nbar\n\n\nfoo\n",
212+
4,
213+
true,
214+
true,
215+
"",
216+
},
217+
// Unless we ask them to be ignored:
218+
{
219+
"\nfoo bar\n\n\nfoo\n",
220+
"foo\nbar\nfoo",
221+
4,
222+
false,
223+
false,
224+
"",
225+
},
226+
{
227+
"test",
228+
"test",
229+
4,
230+
true,
231+
true,
232+
"",
233+
},
234+
{
235+
" ",
236+
" \n \n \n \n \n \n \n \n \n ",
237+
4,
238+
true,
239+
true,
240+
"",
241+
},
242+
}
243+
for i, tc := range tt {
244+
f := NewWriter(tc.Limit)
245+
f.KeepNewlines = tc.KeepNewlines
246+
f.HardWrap = true
247+
f.PreserveSpaces = tc.PreserveSpaces
248+
249+
_, err := f.Write([]byte(tc.Input))
250+
if err != nil {
251+
t.Error(err)
252+
}
253+
f.Close()
254+
255+
if f.String() != tc.Expected {
256+
t.Errorf("Test %d, expected:\n\n`%s`\n\nActual Output:\n\n`%s`", i, tc.Expected, f.String())
257+
}
258+
}
259+
}

0 commit comments

Comments
 (0)