text.go

  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}