text.go

  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}