text.go

  1package text
  2
  3import (
  4	"bytes"
  5	"github.com/mattn/go-runewidth"
  6	"strings"
  7)
  8
  9// Wrap a text for an exact line size
 10// Handle properly terminal color escape code
 11func Wrap(text string, lineWidth int) (string, int) {
 12	return WrapLeftPadded(text, lineWidth, 0)
 13}
 14
 15// Wrap a text for an exact line size with a left padding
 16// Handle properly terminal color escape code
 17func WrapLeftPadded(text string, lineWidth int, leftPad int) (string, int) {
 18	var textBuffer bytes.Buffer
 19	nbLine := 0
 20	pad := strings.Repeat(" ", leftPad)
 21
 22	// tabs are formatted as 4 spaces
 23	text = strings.Replace(text, "\t", "    ", 4)
 24	wrapped := wrapText(text, lineWidth-leftPad)
 25	for _, line := range strings.Split(wrapped, "\n") {
 26		textBuffer.WriteString(pad + line)
 27		textBuffer.WriteString("\n")
 28		nbLine++
 29	}
 30	return textBuffer.String(), nbLine
 31}
 32
 33// Wrap text so that each line fills at most w cells. Lines break at word
 34// boundary or multibyte chars.
 35//
 36// Wrapping Algorithm: Treat the text as a sequence of words, with each word be
 37// an alphanumeric word, or a multibyte char. We scan through the text and
 38// construct the word, and flush the word into the paragraph once a word is
 39// ready. A word is ready when a word boundary is detected: a boundary char such
 40// as '\n', '\t', and ' ' is encountered; a multibyte char is found; or a
 41// multibyte to single-byte switch is encountered. '\n' is handled in a special
 42// manner.
 43func wrapText(s string, w int) string {
 44	word := ""
 45	out := ""
 46
 47	width := 0
 48	firstWord := true
 49	isMultibyteWord := false
 50
 51	flushWord := func() {
 52		wl := wordLen(word)
 53		if isMultibyteWord {
 54			if width+wl > w {
 55				out += "\n" + word
 56				width = wl
 57			} else {
 58				out += word
 59				width += wl
 60			}
 61		} else {
 62			if width == 0 {
 63				out += word
 64				width += wl
 65			} else if width+wl+1 > w {
 66				out += "\n" + word
 67				width = wl
 68			} else {
 69				out += " " + word
 70				width += wl + 1
 71			}
 72		}
 73		word = ""
 74	}
 75
 76	for _, r := range []rune(s) {
 77		cw := runewidth.RuneWidth(r)
 78		if firstWord {
 79			word = string(r)
 80			isMultibyteWord = cw > 1
 81			firstWord = false
 82			continue
 83		}
 84		if r == '\n' {
 85			flushWord()
 86			out += "\n"
 87			width = 0
 88		} else if r == ' ' || r == '\t' {
 89			flushWord()
 90		} else if cw > 1 {
 91			flushWord()
 92			word = string(r)
 93			isMultibyteWord = true
 94			word = string(r)
 95		} else if cw == 1 && isMultibyteWord {
 96			flushWord()
 97			word = string(r)
 98			isMultibyteWord = false
 99		} else {
100			word += string(r)
101		}
102	}
103	// The text may end without newlines, ensure flushing it or we can lose the
104	// last word.
105	flushWord()
106
107	return out
108}
109
110// wordLen return the length of a word, while ignoring the terminal escape
111// sequences
112func wordLen(word string) int {
113	length := 0
114	escape := false
115
116	for _, char := range word {
117		if char == '\x1b' {
118			escape = true
119		}
120		if !escape {
121			length += runewidth.RuneWidth(rune(char))
122		}
123		if char == 'm' {
124			escape = false
125		}
126	}
127
128	return length
129}
130
131// splitWord split a word at the given length, while ignoring the terminal escape sequences
132func splitWord(word string, length int) (string, string) {
133	runes := []rune(word)
134	var result []rune
135	added := 0
136	escape := false
137
138	if length == 0 {
139		return "", word
140	}
141
142	for _, r := range runes {
143		if r == '\x1b' {
144			escape = true
145		}
146
147		width := runewidth.RuneWidth(r)
148		if width+added > length {
149			// wide character made the length overflow
150			break
151		}
152
153		result = append(result, r)
154
155		if !escape {
156			added += width
157			if added >= length {
158				break
159			}
160		}
161
162		if r == 'm' {
163			escape = false
164		}
165	}
166
167	leftover := runes[len(result):]
168
169	return string(result), string(leftover)
170}
171
172func minInt(a, b int) int {
173	if a > b {
174		return b
175	}
176	return a
177}