1package text
2
3import (
4 "bytes"
5 "strings"
6
7 "github.com/mattn/go-runewidth"
8)
9
10// Wrap a text for an exact line size
11// Handle properly terminal color escape code
12func Wrap(text string, lineWidth int) (string, int) {
13 return WrapLeftPadded(text, lineWidth, 0)
14}
15
16// Wrap a text for an exact line size with a left padding
17// Handle properly terminal color escape code
18func WrapLeftPadded(text string, lineWidth int, leftPad int) (string, int) {
19 var textBuffer bytes.Buffer
20 var lineBuffer bytes.Buffer
21 nbLine := 1
22 firstLine := true
23 pad := strings.Repeat(" ", leftPad)
24
25 // tabs are formatted as 4 spaces
26 text = strings.Replace(text, "\t", " ", 4)
27
28 for _, line := range strings.Split(text, "\n") {
29 spaceLeft := lineWidth - leftPad
30
31 if !firstLine {
32 textBuffer.WriteString("\n")
33 nbLine++
34 }
35
36 firstWord := true
37
38 for _, word := range strings.Split(line, " ") {
39 wordLength := wordLen(word)
40
41 if !firstWord {
42 lineBuffer.WriteString(" ")
43 spaceLeft -= 1
44
45 if spaceLeft <= 0 {
46 textBuffer.WriteString(pad + strings.TrimRight(lineBuffer.String(), " "))
47 textBuffer.WriteString("\n")
48 lineBuffer.Reset()
49 spaceLeft = lineWidth - leftPad
50 nbLine++
51 firstLine = false
52 }
53 }
54
55 // Word fit in the current line
56 if spaceLeft >= wordLength {
57 lineBuffer.WriteString(word)
58 spaceLeft -= wordLength
59 firstWord = false
60 } else {
61 // Break a word longer than a line
62 if wordLength > lineWidth {
63 for wordLength > 0 && wordLen(word) > 0 {
64 l := minInt(spaceLeft, wordLength)
65 part, leftover := splitWord(word, l)
66 word = leftover
67 wordLength = wordLen(word)
68
69 lineBuffer.WriteString(part)
70 textBuffer.WriteString(pad)
71 textBuffer.Write(lineBuffer.Bytes())
72 lineBuffer.Reset()
73
74 spaceLeft -= l
75
76 if spaceLeft <= 0 {
77 textBuffer.WriteString("\n")
78 nbLine++
79 spaceLeft = lineWidth - leftPad
80 }
81
82 if wordLength <= 0 {
83 break
84 }
85 }
86 } else {
87 // Normal break
88 textBuffer.WriteString(pad + strings.TrimRight(lineBuffer.String(), " "))
89 textBuffer.WriteString("\n")
90 lineBuffer.Reset()
91 lineBuffer.WriteString(word)
92 firstWord = false
93 spaceLeft = lineWidth - leftPad - wordLength
94 nbLine++
95 }
96 }
97 }
98
99 if lineBuffer.Len() > 0 {
100 textBuffer.WriteString(pad + strings.TrimRight(lineBuffer.String(), " "))
101 lineBuffer.Reset()
102 }
103
104 firstLine = false
105 }
106
107 return textBuffer.String(), nbLine
108}
109
110// wordLen return the length of a word, while ignoring the terminal escape sequences
111func wordLen(word string) int {
112 length := 0
113 escape := false
114
115 for _, char := range word {
116 if char == '\x1b' {
117 escape = true
118 }
119
120 if !escape {
121 length += runewidth.RuneWidth(rune(char))
122 }
123
124 if char == 'm' {
125 escape = false
126 }
127 }
128
129 return length
130}
131
132// splitWord split a word at the given length, while ignoring the terminal escape sequences
133func splitWord(word string, length int) (string, string) {
134 runes := []rune(word)
135 var result []rune
136 added := 0
137 escape := false
138
139 if length == 0 {
140 return "", word
141 }
142
143 for _, r := range runes {
144 if r == '\x1b' {
145 escape = true
146 }
147
148 width := runewidth.RuneWidth(r)
149 if width+added > length {
150 // wide character made the length overflow
151 break
152 }
153
154 result = append(result, r)
155
156 if !escape {
157 added += width
158 if added >= length {
159 break
160 }
161 }
162
163 if r == 'm' {
164 escape = false
165 }
166 }
167
168 leftover := runes[len(result):]
169
170 return string(result), string(leftover)
171}
172
173func minInt(a, b int) int {
174 if a > b {
175 return b
176 }
177 return a
178}