1package text
2
3import (
4 "github.com/mattn/go-runewidth"
5 "strings"
6 "unicode/utf8"
7)
8
9// Wrap a text for an exact line size
10// Handle properly terminal color escape code
11func Wrap(text string, lineWidth int) (string, int) {
12 return WrapLeftPadded(text, lineWidth, 0)
13}
14
15// Wrap a text for an exact line size with a left padding
16// Handle properly terminal color escape code
17func WrapLeftPadded(text string, lineWidth int, leftPad int) (string, int) {
18 pad := strings.Repeat(" ", leftPad)
19 var lines []string
20 nbLine := 0
21
22 // tabs are formatted as 4 spaces
23 text = strings.Replace(text, "\t", " ", -1)
24 for _, line := range strings.Split(text, "\n") {
25 if line == "" || strings.TrimSpace(line) == "" {
26 lines = append(lines, "")
27 nbLine++
28 } else {
29 wrapped := softwrapLine(line, lineWidth-leftPad)
30 firstLine := true
31 for _, seg := range strings.Split(wrapped, "\n") {
32 if firstLine {
33 lines = append(lines, pad+strings.TrimRight(seg, " "))
34 firstLine = false
35 } else {
36 lines = append(lines, pad+strings.TrimSpace(seg))
37 }
38 nbLine++
39 }
40 }
41 }
42 return strings.Join(lines, "\n"), nbLine
43}
44
45type EscapeItem struct {
46 item string
47 pos int
48}
49
50func recordTermEscape(s string) (string, []EscapeItem) {
51 var result []EscapeItem
52 var newStr string
53
54 pos := 0
55 item := ""
56 occupiedRuneCount := 0
57 inEscape := false
58 for i, r := range []rune(s) {
59 if r == '\x1b' {
60 pos = i
61 item = string(r)
62 inEscape = true
63 continue
64 }
65 if inEscape {
66 item += string(r)
67 if r == 'm' {
68 result = append(result, EscapeItem{item: item, pos: pos - occupiedRuneCount})
69 occupiedRuneCount += utf8.RuneCountInString(item)
70 inEscape = false
71 }
72 continue
73 }
74 newStr += string(r)
75 }
76
77 return newStr, result
78}
79
80func replayTermEscape(s string, sequence []EscapeItem) string {
81 if len(sequence) == 0 {
82 return string(s)
83 }
84 // Assume the original string contains no new line and the wrapped only insert
85 // new lines. So that we can recover the position where we insert the term
86 // escapes.
87 var out string = ""
88
89 currPos := 0
90 currItem := 0
91 for _, r := range []rune(s) {
92 if currItem < len(sequence) && currPos == sequence[currItem].pos {
93 if r == '\n' {
94 out += "\n" + sequence[currItem].item
95 } else {
96 out += sequence[currItem].item + string(r)
97 currPos++
98 }
99 currItem++
100 } else {
101 if r != '\n' {
102 currPos++
103 }
104 out += string(r)
105 }
106 }
107
108 return out
109}
110
111// Break a line into several lines so that each line consumes at most 'w' cells.
112// Lines break at group of white spaces and multibyte chars. Nothing is removed
113// from the line so that it behaves like a softwrap.
114//
115// Required: The line shall not contain '\n' (so it is a single line).
116//
117// WRAPPING ALGORITHM: The line is broken into non-breakable groups, then line
118// breaks ("\n") is inserted between these groups so that the total length
119// between breaks does not exceed the required width. Words that are longer than
120// the width is broken into several words as `M+M+...+N`.
121func softwrapLine(s string, w int) string {
122 newStr, termSeqs := recordTermEscape(s)
123
124 const (
125 WIDE_CHAR = iota
126 INVISIBLE = iota
127 SHORT_UNICODE = iota
128 SPACE = iota
129 VISIBLE_ASCII = iota
130 NONE = iota
131 )
132
133 // In order to simplify the terminal color sequence handling, we first strip
134 // them out of the text and record their position, then do the wrap. After
135 // that, we insert back these sequences.
136 runeType := func(r rune) int {
137 rw := runewidth.RuneWidth(r)
138 if rw > 1 {
139 return WIDE_CHAR
140 } else if rw == 0 {
141 return INVISIBLE
142 } else if r > 127 {
143 return SHORT_UNICODE
144 } else if r == ' ' {
145 return SPACE
146 } else {
147 return VISIBLE_ASCII
148 }
149 }
150
151 var chunks []string
152 var word string
153 wordType := NONE
154 flushWord := func() {
155 chunks = append(chunks, word)
156 word = ""
157 wordType = NONE
158 }
159 for _, r := range []rune(newStr) {
160 // A WIDE_CHAR itself constitutes a group.
161 thisType := runeType(r)
162 if thisType == WIDE_CHAR {
163 if wordType != NONE {
164 flushWord()
165 }
166 chunks = append(chunks, string(r))
167 continue
168 }
169 // Other type of groups starts with a char of that type, and ends with a
170 // char with different type or end of string.
171 if thisType != wordType {
172 if wordType != NONE {
173 flushWord()
174 }
175 word = string(r)
176 wordType = thisType
177 } else {
178 word += string(r)
179 }
180 }
181 if word != "" {
182 flushWord()
183 }
184
185 var line string = ""
186 var width int = 0
187 // Reverse the chunk array so we can use it as a stack.
188 for i, j := 0, len(chunks)-1; i < j; i, j = i+1, j-1 {
189 chunks[i], chunks[j] = chunks[j], chunks[i]
190 }
191 for len(chunks) > 0 {
192 thisWord := chunks[len(chunks)-1]
193 wl := wordLen(thisWord)
194 if width+wl <= w {
195 line += chunks[len(chunks)-1]
196 chunks = chunks[:len(chunks)-1]
197 width += wl
198 if width == w && len(chunks) > 0 {
199 line += "\n"
200 width = 0
201 }
202 } else if wl > w {
203 left, right := splitWord(chunks[len(chunks)-1], w)
204 line += left + "\n"
205 chunks[len(chunks)-1] = right
206 width = 0
207 } else {
208 line += "\n"
209 width = 0
210 }
211 }
212
213 line = replayTermEscape(line, termSeqs)
214 return line
215}
216
217// wordLen return the length of a word, while ignoring the terminal escape
218// sequences
219func wordLen(word string) int {
220 length := 0
221 escape := false
222
223 for _, char := range word {
224 if char == '\x1b' {
225 escape = true
226 }
227 if !escape {
228 length += runewidth.RuneWidth(rune(char))
229 }
230 if char == 'm' {
231 escape = false
232 }
233 }
234
235 return length
236}
237
238// splitWord split a word at the given length, while ignoring the terminal escape sequences
239func splitWord(word string, length int) (string, string) {
240 runes := []rune(word)
241 var result []rune
242 added := 0
243 escape := false
244
245 if length == 0 {
246 return "", word
247 }
248
249 for _, r := range runes {
250 if r == '\x1b' {
251 escape = true
252 }
253
254 width := runewidth.RuneWidth(r)
255 if width+added > length {
256 // wide character made the length overflow
257 break
258 }
259
260 result = append(result, r)
261
262 if !escape {
263 added += width
264 if added >= length {
265 break
266 }
267 }
268
269 if r == 'm' {
270 escape = false
271 }
272 }
273
274 leftover := runes[len(result):]
275
276 return string(result), string(leftover)
277}
278
279func minInt(a, b int) int {
280 if a > b {
281 return b
282 }
283 return a
284}