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 && len(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				} else {
 81					// Normal break
 82					textBuffer.WriteString(pad + strings.TrimRight(lineBuffer.String(), " "))
 83					textBuffer.WriteString("\n")
 84					lineBuffer.Reset()
 85					lineBuffer.WriteString(word)
 86					firstWord = false
 87					spaceLeft = lineWidth - leftPad - wordLength
 88					nbLine++
 89				}
 90			}
 91		}
 92
 93		textBuffer.WriteString(pad + strings.TrimRight(lineBuffer.String(), " "))
 94		lineBuffer.Reset()
 95		firstLine = false
 96	}
 97
 98	return textBuffer.String(), nbLine
 99}
100
101// wordLen return the length of a word, while ignoring the terminal escape sequences
102func wordLen(word string) int {
103	length := 0
104	escape := false
105
106	for _, char := range word {
107		if char == '\x1b' {
108			escape = true
109		}
110
111		if !escape {
112			length++
113		}
114
115		if char == 'm' {
116			escape = false
117		}
118	}
119
120	return length
121}
122
123// splitWord split a word at the given length, while ignoring the terminal escape sequences
124func splitWord(word string, length int) (string, string) {
125	runes := []rune(word)
126	var result []rune
127	added := 0
128	escape := false
129
130	if length == 0 {
131		return "", word
132	}
133
134	for _, r := range runes {
135		if r == '\x1b' {
136			escape = true
137		}
138
139		result = append(result, r)
140
141		if !escape {
142			added++
143			if added == length {
144				break
145			}
146		}
147
148		if r == 'm' {
149			escape = false
150		}
151	}
152
153	leftover := runes[len(result):]
154
155	return string(result), string(leftover)
156}
157
158func minInt(a, b int) int {
159	if a > b {
160		return b
161	}
162	return a
163}