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