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 []rune(word) {
117 if char == '\x1b' {
118 escape = true
119 }
120 if !escape {
121 length += runewidth.RuneWidth(char)
122 }
123 if char == 'm' {
124 escape = false
125 }
126 }
127
128 return length
129}