...

Source file src/github.com/jedib0t/go-pretty/v6/text/wrap.go

Documentation: github.com/jedib0t/go-pretty/v6/text

     1  package text
     2  
     3  import (
     4  	"strings"
     5  	"unicode/utf8"
     6  )
     7  
     8  // WrapHard wraps a string to the given length using a newline. Handles strings
     9  // with ANSI escape sequences (such as text color) without breaking the text
    10  // formatting. Breaks all words that go beyond the line boundary.
    11  //
    12  // For examples, refer to the unit-tests or GoDoc examples.
    13  func WrapHard(str string, wrapLen int) string {
    14  	if wrapLen <= 0 {
    15  		return ""
    16  	}
    17  	str = strings.Replace(str, "\t", "    ", -1)
    18  	sLen := utf8.RuneCountInString(str)
    19  	if sLen <= wrapLen {
    20  		return str
    21  	}
    22  
    23  	out := &strings.Builder{}
    24  	out.Grow(sLen + (sLen / wrapLen))
    25  	for idx, paragraph := range strings.Split(str, "\n\n") {
    26  		if idx > 0 {
    27  			out.WriteString("\n\n")
    28  		}
    29  		wrapHard(paragraph, wrapLen, out)
    30  	}
    31  
    32  	return out.String()
    33  }
    34  
    35  // WrapSoft wraps a string to the given length using a newline. Handles strings
    36  // with ANSI escape sequences (such as text color) without breaking the text
    37  // formatting. Tries to move words that go beyond the line boundary to the next
    38  // line.
    39  //
    40  // For examples, refer to the unit-tests or GoDoc examples.
    41  func WrapSoft(str string, wrapLen int) string {
    42  	if wrapLen <= 0 {
    43  		return ""
    44  	}
    45  	str = strings.Replace(str, "\t", "    ", -1)
    46  	sLen := utf8.RuneCountInString(str)
    47  	if sLen <= wrapLen {
    48  		return str
    49  	}
    50  
    51  	out := &strings.Builder{}
    52  	out.Grow(sLen + (sLen / wrapLen))
    53  	for idx, paragraph := range strings.Split(str, "\n\n") {
    54  		if idx > 0 {
    55  			out.WriteString("\n\n")
    56  		}
    57  		wrapSoft(paragraph, wrapLen, out)
    58  	}
    59  
    60  	return out.String()
    61  }
    62  
    63  // WrapText is very similar to WrapHard except for one minor difference. Unlike
    64  // WrapHard which discards line-breaks and respects only paragraph-breaks, this
    65  // function respects line-breaks too.
    66  //
    67  // For examples, refer to the unit-tests or GoDoc examples.
    68  func WrapText(str string, wrapLen int) string {
    69  	if wrapLen <= 0 {
    70  		return ""
    71  	}
    72  
    73  	var out strings.Builder
    74  	sLen := utf8.RuneCountInString(str)
    75  	out.Grow(sLen + (sLen / wrapLen))
    76  	lineIdx, isEscSeq, lastEscSeq := 0, false, ""
    77  	for _, char := range str {
    78  		if char == EscapeStartRune {
    79  			isEscSeq = true
    80  			lastEscSeq = ""
    81  		}
    82  		if isEscSeq {
    83  			lastEscSeq += string(char)
    84  		}
    85  
    86  		appendChar(char, wrapLen, &lineIdx, isEscSeq, lastEscSeq, &out)
    87  
    88  		if isEscSeq && char == EscapeStopRune {
    89  			isEscSeq = false
    90  		}
    91  		if lastEscSeq == EscapeReset {
    92  			lastEscSeq = ""
    93  		}
    94  	}
    95  	if lastEscSeq != "" && lastEscSeq != EscapeReset {
    96  		out.WriteString(EscapeReset)
    97  	}
    98  	return out.String()
    99  }
   100  
   101  func appendChar(char rune, wrapLen int, lineLen *int, inEscSeq bool, lastSeenEscSeq string, out *strings.Builder) {
   102  	// handle reaching the end of the line as dictated by wrapLen or by finding
   103  	// a newline character
   104  	if (*lineLen == wrapLen && !inEscSeq && char != '\n') || (char == '\n') {
   105  		if lastSeenEscSeq != "" {
   106  			// terminate escape sequence and the line; and restart the escape
   107  			// sequence in the next line
   108  			out.WriteString(EscapeReset)
   109  			out.WriteRune('\n')
   110  			out.WriteString(lastSeenEscSeq)
   111  		} else {
   112  			// just start a new line
   113  			out.WriteRune('\n')
   114  		}
   115  		// reset line index to 0th character
   116  		*lineLen = 0
   117  	}
   118  
   119  	// if the rune is not a new line, output it
   120  	if char != '\n' {
   121  		out.WriteRune(char)
   122  
   123  		// increment the line index if not in the middle of an escape sequence
   124  		if !inEscSeq {
   125  			*lineLen++
   126  		}
   127  	}
   128  }
   129  
   130  func appendWord(word string, lineIdx *int, lastSeenEscSeq string, wrapLen int, out *strings.Builder) {
   131  	inEscSeq := false
   132  	for _, char := range word {
   133  		if char == EscapeStartRune {
   134  			inEscSeq = true
   135  			lastSeenEscSeq = ""
   136  		}
   137  		if inEscSeq {
   138  			lastSeenEscSeq += string(char)
   139  		}
   140  
   141  		appendChar(char, wrapLen, lineIdx, inEscSeq, lastSeenEscSeq, out)
   142  
   143  		if inEscSeq && char == EscapeStopRune {
   144  			inEscSeq = false
   145  		}
   146  		if lastSeenEscSeq == EscapeReset {
   147  			lastSeenEscSeq = ""
   148  		}
   149  	}
   150  }
   151  
   152  func extractOpenEscapeSeq(str string) string {
   153  	escapeSeq, inEscSeq := "", false
   154  	for _, char := range str {
   155  		if char == EscapeStartRune {
   156  			inEscSeq = true
   157  			escapeSeq = ""
   158  		}
   159  		if inEscSeq {
   160  			escapeSeq += string(char)
   161  		}
   162  		if char == EscapeStopRune {
   163  			inEscSeq = false
   164  		}
   165  	}
   166  	if escapeSeq == EscapeReset {
   167  		escapeSeq = ""
   168  	}
   169  	return escapeSeq
   170  }
   171  
   172  func terminateLine(wrapLen int, lineLen *int, lastSeenEscSeq string, out *strings.Builder) {
   173  	if *lineLen < wrapLen {
   174  		out.WriteString(strings.Repeat(" ", wrapLen-*lineLen))
   175  	}
   176  	// something is already on the line; terminate it
   177  	if lastSeenEscSeq != "" {
   178  		out.WriteString(EscapeReset)
   179  	}
   180  	out.WriteRune('\n')
   181  	out.WriteString(lastSeenEscSeq)
   182  	*lineLen = 0
   183  }
   184  
   185  func terminateOutput(lastSeenEscSeq string, out *strings.Builder) {
   186  	if lastSeenEscSeq != "" && lastSeenEscSeq != EscapeReset && !strings.HasSuffix(out.String(), EscapeReset) {
   187  		out.WriteString(EscapeReset)
   188  	}
   189  }
   190  
   191  func wrapHard(paragraph string, wrapLen int, out *strings.Builder) {
   192  	lineLen, lastSeenEscSeq := 0, ""
   193  	words := strings.Fields(paragraph)
   194  	for wordIdx, word := range words {
   195  		escSeq := extractOpenEscapeSeq(word)
   196  		if escSeq != "" {
   197  			lastSeenEscSeq = escSeq
   198  		}
   199  		if lineLen > 0 {
   200  			out.WriteRune(' ')
   201  			lineLen++
   202  		}
   203  
   204  		wordLen := RuneWidthWithoutEscSequences(word)
   205  		if lineLen+wordLen <= wrapLen { // word fits within the line
   206  			out.WriteString(word)
   207  			lineLen += wordLen
   208  		} else { // word doesn't fit within the line; hard-wrap
   209  			appendWord(word, &lineLen, lastSeenEscSeq, wrapLen, out)
   210  		}
   211  
   212  		// end of line; but more words incoming
   213  		if lineLen == wrapLen && wordIdx < len(words)-1 {
   214  			terminateLine(wrapLen, &lineLen, lastSeenEscSeq, out)
   215  		}
   216  	}
   217  	terminateOutput(lastSeenEscSeq, out)
   218  }
   219  
   220  func wrapSoft(paragraph string, wrapLen int, out *strings.Builder) {
   221  	lineLen, lastSeenEscSeq := 0, ""
   222  	words := strings.Fields(paragraph)
   223  	for wordIdx, word := range words {
   224  		escSeq := extractOpenEscapeSeq(word)
   225  		if escSeq != "" {
   226  			lastSeenEscSeq = escSeq
   227  		}
   228  
   229  		spacing, spacingLen := wrapSoftSpacing(lineLen)
   230  		wordLen := RuneWidthWithoutEscSequences(word)
   231  		if lineLen+spacingLen+wordLen <= wrapLen { // word fits within the line
   232  			out.WriteString(spacing)
   233  			out.WriteString(word)
   234  			lineLen += spacingLen + wordLen
   235  		} else { // word doesn't fit within the line
   236  			lineLen = wrapSoftLastWordInLine(wrapLen, lineLen, lastSeenEscSeq, wordLen, word, out)
   237  		}
   238  
   239  		// end of line; but more words incoming
   240  		if lineLen == wrapLen && wordIdx < len(words)-1 {
   241  			terminateLine(wrapLen, &lineLen, lastSeenEscSeq, out)
   242  		}
   243  	}
   244  	terminateOutput(lastSeenEscSeq, out)
   245  }
   246  
   247  func wrapSoftLastWordInLine(wrapLen int, lineLen int, lastSeenEscSeq string, wordLen int, word string, out *strings.Builder) int {
   248  	if lineLen > 0 { // something is already on the line; terminate it
   249  		terminateLine(wrapLen, &lineLen, lastSeenEscSeq, out)
   250  	}
   251  	if wordLen <= wrapLen { // word fits within a single line
   252  		out.WriteString(word)
   253  		lineLen = wordLen
   254  	} else { // word doesn't fit within a single line; hard-wrap
   255  		appendWord(word, &lineLen, lastSeenEscSeq, wrapLen, out)
   256  	}
   257  	return lineLen
   258  }
   259  
   260  func wrapSoftSpacing(lineLen int) (string, int) {
   261  	spacing, spacingLen := "", 0
   262  	if lineLen > 0 {
   263  		spacing, spacingLen = " ", 1
   264  	}
   265  	return spacing, spacingLen
   266  }
   267  

View as plain text