aboutsummaryrefslogblamecommitdiffstats
path: root/util/text/text.go
blob: cffb4ee21622eed6304a649c3d9ef3a865430ebf (plain) (tree)
1
2
3
4
5
6
7
8
9
            



                 

                                       

 

                                             

                                                     

 

                                                         
                                                                            



                                   
                                           




                                                        
                                                








                                                               




                                                           
 






                                                                                                                 
                                 





                                                            

                                                 

                                                                  
                                                                                 



                                                                                    

                                                                            
                                                                           


                                                                                    


                                                                   

                                                                                    
                                                                                       
                                                 



                                                                    

                                         
                                                       
                                                                                                                 

                                                                    
                                                                    
                                                         
                                                                                    



                                                
 




                                                                                                 





                                          
                                                                                    









                                     
                                                                 









                                      
                                                                                           
                                                          

                             






                               

                                 


                                     





                                                                  
                                          

                            

                                            



                                     
                             



                                      
                                       
 
                                               

 





                           
package text

import (
	"bytes"
	"strings"

	"github.com/mattn/go-runewidth"
)

// Wrap a text for an exact line size
// Handle properly terminal color escape code
func Wrap(text string, lineWidth int) (string, int) {
	return WrapLeftPadded(text, lineWidth, 0)
}

// Wrap a text for an exact line size with a left padding
// Handle properly terminal color escape code
func WrapLeftPadded(text string, lineWidth int, leftPad int) (string, int) {
	var textBuffer bytes.Buffer
	var lineBuffer bytes.Buffer
	nbLine := 1
	firstLine := true
	pad := strings.Repeat(" ", leftPad)

	// tabs are formatted as 4 spaces
	text = strings.Replace(text, "\t", "    ", 4)

	for _, line := range strings.Split(text, "\n") {
		spaceLeft := lineWidth - leftPad

		if !firstLine {
			textBuffer.WriteString("\n")
			nbLine++
		}

		firstWord := true

		for _, word := range strings.Split(line, " ") {
			wordLength := wordLen(word)

			if !firstWord {
				lineBuffer.WriteString(" ")
				spaceLeft -= 1

				if spaceLeft <= 0 {
					textBuffer.WriteString(pad + strings.TrimRight(lineBuffer.String(), " "))
					textBuffer.WriteString("\n")
					lineBuffer.Reset()
					spaceLeft = lineWidth - leftPad
					nbLine++
					firstLine = false
				}
			}

			// Word fit in the current line
			if spaceLeft >= wordLength {
				lineBuffer.WriteString(word)
				spaceLeft -= wordLength
				firstWord = false
			} else {
				// Break a word longer than a line
				if wordLength > lineWidth {
					for wordLength > 0 && wordLen(word) > 0 {
						l := minInt(spaceLeft, wordLength)
						part, leftover := splitWord(word, l)
						word = leftover
						wordLength = wordLen(word)

						lineBuffer.WriteString(part)
						textBuffer.WriteString(pad)
						textBuffer.Write(lineBuffer.Bytes())
						lineBuffer.Reset()

						spaceLeft -= l

						if spaceLeft <= 0 {
							textBuffer.WriteString("\n")
							nbLine++
							spaceLeft = lineWidth - leftPad
						}

						if wordLength <= 0 {
							break
						}
					}
				} else {
					// Normal break
					textBuffer.WriteString(pad + strings.TrimRight(lineBuffer.String(), " "))
					textBuffer.WriteString("\n")
					lineBuffer.Reset()
					lineBuffer.WriteString(word)
					firstWord = false
					spaceLeft = lineWidth - leftPad - wordLength
					nbLine++
				}
			}
		}

		if lineBuffer.Len() > 0 {
			textBuffer.WriteString(pad + strings.TrimRight(lineBuffer.String(), " "))
			lineBuffer.Reset()
		}

		firstLine = false
	}

	return textBuffer.String(), nbLine
}

// wordLen return the length of a word, while ignoring the terminal escape sequences
func wordLen(word string) int {
	length := 0
	escape := false

	for _, char := range word {
		if char == '\x1b' {
			escape = true
		}

		if !escape {
			length += runewidth.RuneWidth(rune(char))
		}

		if char == 'm' {
			escape = false
		}
	}

	return length
}

// splitWord split a word at the given length, while ignoring the terminal escape sequences
func splitWord(word string, length int) (string, string) {
	runes := []rune(word)
	var result []rune
	added := 0
	escape := false

	if length == 0 {
		return "", word
	}

	for _, r := range runes {
		if r == '\x1b' {
			escape = true
		}

		width := runewidth.RuneWidth(r)
		if width+added > length {
			// wide character made the length overflow
			break
		}

		result = append(result, r)

		if !escape {
			added += width
			if added >= length {
				break
			}
		}

		if r == 'm' {
			escape = false
		}
	}

	leftover := runes[len(result):]

	return string(result), string(leftover)
}

func minInt(a, b int) int {
	if a > b {
		return b
	}
	return a
}