...

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

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

     1  package text
     2  
     3  import (
     4  	"strings"
     5  	"unicode/utf8"
     6  
     7  	"github.com/mattn/go-runewidth"
     8  )
     9  
    10  // Constants
    11  const (
    12  	EscapeReset     = EscapeStart + "0" + EscapeStop
    13  	EscapeStart     = "\x1b["
    14  	EscapeStartRune = rune(27) // \x1b
    15  	EscapeStop      = "m"
    16  	EscapeStopRune  = 'm'
    17  )
    18  
    19  // RuneWidth stuff
    20  var (
    21  	rwCondition = runewidth.NewCondition()
    22  )
    23  
    24  // InsertEveryN inserts the rune every N characters in the string. For ex.:
    25  //  InsertEveryN("Ghost", '-', 1) == "G-h-o-s-t"
    26  //  InsertEveryN("Ghost", '-', 2) == "Gh-os-t"
    27  //  InsertEveryN("Ghost", '-', 3) == "Gho-st"
    28  //  InsertEveryN("Ghost", '-', 4) == "Ghos-t"
    29  //  InsertEveryN("Ghost", '-', 5) == "Ghost"
    30  func InsertEveryN(str string, runeToInsert rune, n int) string {
    31  	if n <= 0 {
    32  		return str
    33  	}
    34  
    35  	sLen := RuneWidthWithoutEscSequences(str)
    36  	var out strings.Builder
    37  	out.Grow(sLen + (sLen / n))
    38  	outLen, isEscSeq := 0, false
    39  	for idx, c := range str {
    40  		if c == EscapeStartRune {
    41  			isEscSeq = true
    42  		}
    43  
    44  		if !isEscSeq && outLen > 0 && (outLen%n) == 0 && idx != sLen {
    45  			out.WriteRune(runeToInsert)
    46  		}
    47  		out.WriteRune(c)
    48  		if !isEscSeq {
    49  			outLen += RuneWidth(c)
    50  		}
    51  
    52  		if isEscSeq && c == EscapeStopRune {
    53  			isEscSeq = false
    54  		}
    55  	}
    56  	return out.String()
    57  }
    58  
    59  // LongestLineLen returns the length of the longest "line" within the
    60  // argument string. For ex.:
    61  //  LongestLineLen("Ghost!\nCome back here!\nRight now!") == 15
    62  func LongestLineLen(str string) int {
    63  	maxLength, currLength, isEscSeq := 0, 0, false
    64  	for _, c := range str {
    65  		if c == EscapeStartRune {
    66  			isEscSeq = true
    67  		} else if isEscSeq && c == EscapeStopRune {
    68  			isEscSeq = false
    69  			continue
    70  		}
    71  
    72  		if c == '\n' {
    73  			if currLength > maxLength {
    74  				maxLength = currLength
    75  			}
    76  			currLength = 0
    77  		} else if !isEscSeq {
    78  			currLength += RuneWidth(c)
    79  		}
    80  	}
    81  	if currLength > maxLength {
    82  		maxLength = currLength
    83  	}
    84  	return maxLength
    85  }
    86  
    87  // OverrideRuneWidthEastAsianWidth can *probably* help with alignment, and
    88  // length calculation issues when dealing with Unicode character-set and a
    89  // non-English language set in the LANG variable.
    90  //
    91  // Set this to 'false' to force the "runewidth" library to pretend to deal with
    92  // English character-set. Be warned that if the text/content you are dealing
    93  // with contains East Asian character-set, this may result in unexpected
    94  // behavior.
    95  //
    96  // References:
    97  // * https://github.com/mattn/go-runewidth/issues/64#issuecomment-1221642154
    98  // * https://github.com/jedib0t/go-pretty/issues/220
    99  // * https://github.com/jedib0t/go-pretty/issues/204
   100  func OverrideRuneWidthEastAsianWidth(val bool) {
   101  	rwCondition.EastAsianWidth = val
   102  }
   103  
   104  // Pad pads the given string with as many characters as needed to make it as
   105  // long as specified (maxLen). This function does not count escape sequences
   106  // while calculating length of the string. Ex.:
   107  //  Pad("Ghost", 0, ' ') == "Ghost"
   108  //  Pad("Ghost", 3, ' ') == "Ghost"
   109  //  Pad("Ghost", 5, ' ') == "Ghost"
   110  //  Pad("Ghost", 7, ' ') == "Ghost  "
   111  //  Pad("Ghost", 10, '.') == "Ghost....."
   112  func Pad(str string, maxLen int, paddingChar rune) string {
   113  	strLen := RuneWidthWithoutEscSequences(str)
   114  	if strLen < maxLen {
   115  		str += strings.Repeat(string(paddingChar), maxLen-strLen)
   116  	}
   117  	return str
   118  }
   119  
   120  // RepeatAndTrim repeats the given string until it is as long as maxRunes.
   121  // For ex.:
   122  //  RepeatAndTrim("", 5) == ""
   123  //  RepeatAndTrim("Ghost", 0) == ""
   124  //  RepeatAndTrim("Ghost", 5) == "Ghost"
   125  //  RepeatAndTrim("Ghost", 7) == "GhostGh"
   126  //  RepeatAndTrim("Ghost", 10) == "GhostGhost"
   127  func RepeatAndTrim(str string, maxRunes int) string {
   128  	if str == "" || maxRunes == 0 {
   129  		return ""
   130  	} else if maxRunes == utf8.RuneCountInString(str) {
   131  		return str
   132  	}
   133  	repeatedS := strings.Repeat(str, int(maxRunes/utf8.RuneCountInString(str))+1)
   134  	return Trim(repeatedS, maxRunes)
   135  }
   136  
   137  // RuneCount is similar to utf8.RuneCountInString, except for the fact that it
   138  // ignores escape sequences while counting. For ex.:
   139  //  RuneCount("") == 0
   140  //  RuneCount("Ghost") == 5
   141  //  RuneCount("\x1b[33mGhost\x1b[0m") == 5
   142  //  RuneCount("\x1b[33mGhost\x1b[0") == 5
   143  // Deprecated: in favor of RuneWidthWithoutEscSequences
   144  func RuneCount(str string) int {
   145  	return RuneWidthWithoutEscSequences(str)
   146  }
   147  
   148  // RuneWidth returns the mostly accurate character-width of the rune. This is
   149  // not 100% accurate as the character width is usually dependent on the
   150  // typeface (font) used in the console/terminal. For ex.:
   151  //  RuneWidth('A') == 1
   152  //  RuneWidth('ツ') == 2
   153  //  RuneWidth('⊙') == 1
   154  //  RuneWidth('︿') == 2
   155  //  RuneWidth(0x27) == 0
   156  func RuneWidth(r rune) int {
   157  	return rwCondition.RuneWidth(r)
   158  }
   159  
   160  // RuneWidthWithoutEscSequences is similar to RuneWidth, except for the fact
   161  // that it ignores escape sequences while counting. For ex.:
   162  //  RuneWidthWithoutEscSequences("") == 0
   163  //  RuneWidthWithoutEscSequences("Ghost") == 5
   164  //  RuneWidthWithoutEscSequences("\x1b[33mGhost\x1b[0m") == 5
   165  //  RuneWidthWithoutEscSequences("\x1b[33mGhost\x1b[0") == 5
   166  func RuneWidthWithoutEscSequences(str string) int {
   167  	count, isEscSeq := 0, false
   168  	for _, c := range str {
   169  		if c == EscapeStartRune {
   170  			isEscSeq = true
   171  		} else if isEscSeq {
   172  			if c == EscapeStopRune {
   173  				isEscSeq = false
   174  			}
   175  		} else {
   176  			count += RuneWidth(c)
   177  		}
   178  	}
   179  	return count
   180  }
   181  
   182  // Snip returns the given string with a fixed length. For ex.:
   183  //  Snip("Ghost", 0, "~") == "Ghost"
   184  //  Snip("Ghost", 1, "~") == "~"
   185  //  Snip("Ghost", 3, "~") == "Gh~"
   186  //  Snip("Ghost", 5, "~") == "Ghost"
   187  //  Snip("Ghost", 7, "~") == "Ghost  "
   188  //  Snip("\x1b[33mGhost\x1b[0m", 7, "~") == "\x1b[33mGhost\x1b[0m  "
   189  func Snip(str string, length int, snipIndicator string) string {
   190  	if length > 0 {
   191  		lenStr := RuneWidthWithoutEscSequences(str)
   192  		if lenStr > length {
   193  			lenStrFinal := length - RuneWidthWithoutEscSequences(snipIndicator)
   194  			return Trim(str, lenStrFinal) + snipIndicator
   195  		}
   196  	}
   197  	return str
   198  }
   199  
   200  // Trim trims a string to the given length while ignoring escape sequences. For
   201  // ex.:
   202  //  Trim("Ghost", 3) == "Gho"
   203  //  Trim("Ghost", 6) == "Ghost"
   204  //  Trim("\x1b[33mGhost\x1b[0m", 3) == "\x1b[33mGho\x1b[0m"
   205  //  Trim("\x1b[33mGhost\x1b[0m", 6) == "\x1b[33mGhost\x1b[0m"
   206  func Trim(str string, maxLen int) string {
   207  	if maxLen <= 0 {
   208  		return ""
   209  	}
   210  
   211  	var out strings.Builder
   212  	out.Grow(maxLen)
   213  
   214  	outLen, isEscSeq, lastEscSeq := 0, false, strings.Builder{}
   215  	for _, sChr := range str {
   216  		out.WriteRune(sChr)
   217  		if sChr == EscapeStartRune {
   218  			isEscSeq = true
   219  			lastEscSeq.Reset()
   220  			lastEscSeq.WriteRune(sChr)
   221  		} else if isEscSeq {
   222  			lastEscSeq.WriteRune(sChr)
   223  			if sChr == EscapeStopRune {
   224  				isEscSeq = false
   225  			}
   226  		} else {
   227  			outLen++
   228  			if outLen == maxLen {
   229  				break
   230  			}
   231  		}
   232  	}
   233  	if lastEscSeq.Len() > 0 && lastEscSeq.String() != EscapeReset {
   234  		out.WriteString(EscapeReset)
   235  	}
   236  	return out.String()
   237  }
   238  

View as plain text