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 []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}