aboutsummaryrefslogtreecommitdiffstats
path: root/util/text
diff options
context:
space:
mode:
Diffstat (limited to 'util/text')
-rw-r--r--util/text/left_padded.go20
-rw-r--r--util/text/text.go160
-rw-r--r--util/text/text_test.go191
3 files changed, 371 insertions, 0 deletions
diff --git a/util/text/left_padded.go b/util/text/left_padded.go
new file mode 100644
index 00000000..a38dfba5
--- /dev/null
+++ b/util/text/left_padded.go
@@ -0,0 +1,20 @@
+package text
+
+import (
+ "strings"
+ "unicode/utf8"
+)
+
+// LeftPaddedString pads a string on the left by a specified amount and pads the string on the right to fill the maxLength
+func LeftPaddedString(value string, maxValueLength, padAmount int) string {
+ valueLength := utf8.RuneCountInString(value)
+ if maxValueLength-padAmount >= valueLength {
+ return strings.Repeat(" ", padAmount) + value + strings.Repeat(" ", maxValueLength-valueLength-padAmount)
+ } else if maxValueLength-padAmount < valueLength {
+ tmp := strings.Trim(value[0:maxValueLength-padAmount-3], " ") + "..."
+ tmpLength := utf8.RuneCountInString(tmp)
+ return strings.Repeat(" ", padAmount) + tmp + strings.Repeat(" ", maxValueLength-tmpLength-padAmount)
+ }
+
+ return value
+}
diff --git a/util/text/text.go b/util/text/text.go
new file mode 100644
index 00000000..10b70b01
--- /dev/null
+++ b/util/text/text.go
@@ -0,0 +1,160 @@
+package text
+
+import (
+ "bytes"
+ "strings"
+)
+
+// 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 && len(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
+ }
+ }
+ } else {
+ // Normal break
+ textBuffer.WriteString(pad + strings.TrimRight(lineBuffer.String(), " "))
+ textBuffer.WriteString("\n")
+ lineBuffer.Reset()
+ lineBuffer.WriteString(word)
+ firstWord = false
+ spaceLeft = lineWidth - wordLength
+ nbLine++
+ }
+ }
+ }
+
+ textBuffer.WriteString(pad + strings.TrimRight(lineBuffer.String(), " "))
+ lineBuffer.Reset()
+ firstLine = false
+ }
+
+ return textBuffer.String(), nbLine
+}
+
+func wordLen(word string) int {
+ length := 0
+ escape := false
+
+ for _, char := range word {
+ if char == '\x1b' {
+ escape = true
+ }
+
+ if !escape {
+ length++
+ }
+
+ if char == 'm' {
+ escape = false
+ }
+ }
+
+ return length
+}
+
+func splitWord(word string, length int) (string, string) {
+ result := ""
+ added := 0
+ escape := false
+
+ if length == 0 {
+ return "", word
+ }
+
+ for _, char := range word {
+ if char == '\x1b' {
+ escape = true
+ }
+
+ result += string(char)
+
+ if !escape {
+ added++
+ if added == length {
+ break
+ }
+ }
+
+ if char == 'm' {
+ escape = false
+ }
+ }
+
+ leftover := word[len(result):]
+
+ return result, leftover
+}
+
+func minInt(a, b int) int {
+ if a > b {
+ return b
+ }
+ return a
+}
diff --git a/util/text/text_test.go b/util/text/text_test.go
new file mode 100644
index 00000000..96234464
--- /dev/null
+++ b/util/text/text_test.go
@@ -0,0 +1,191 @@
+package text
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestWrap(t *testing.T) {
+ cases := []struct {
+ Input, Output string
+ Lim int
+ }{
+ // A simple word passes through.
+ {
+ "foo",
+ "foo",
+ 4,
+ },
+ // Word breaking
+ {
+ "foobarbaz",
+ "foob\narba\nz",
+ 4,
+ },
+ // Lines are broken at whitespace.
+ {
+ "foo bar baz",
+ "foo\nbar\nbaz",
+ 4,
+ },
+ // Word breaking
+ {
+ "foo bars bazzes",
+ "foo\nbars\nbazz\nes",
+ 4,
+ },
+ // A word that would run beyond the width is wrapped.
+ {
+ "fo sop",
+ "fo\nsop",
+ 4,
+ },
+ // A tab counts as 4 characters.
+ {
+ "foo\nb\t r\n baz",
+ "foo\nb\n r\n baz",
+ 4,
+ },
+ // Trailing whitespace is removed after used for wrapping.
+ // Runs of whitespace on which a line is broken are removed.
+ {
+ "foo \nb ar ",
+ "foo\n\nb\nar\n",
+ 4,
+ },
+ // An explicit line break at the end of the input is preserved.
+ {
+ "foo bar baz\n",
+ "foo\nbar\nbaz\n",
+ 4,
+ },
+ // Explicit break are always preserved.
+ {
+ "\nfoo bar\n\n\nbaz\n",
+ "\nfoo\nbar\n\n\nbaz\n",
+ 4,
+ },
+ // Ignore complete words with terminal color sequence
+ {
+ "foo \x1b[31mbar\x1b[0m baz",
+ "foo\n\x1b[31mbar\x1b[0m\nbaz",
+ 4,
+ },
+ // Handle words with colors sequence inside the word
+ {
+ "foo b\x1b[31mbar\x1b[0mr baz",
+ "foo\nb\x1b[31mbar\n\x1b[0mr\nbaz",
+ 4,
+ },
+ // Break words with colors sequence inside the word
+ {
+ "foo bb\x1b[31mbar\x1b[0mr baz",
+ "foo\nbb\x1b[31mba\nr\x1b[0mr\nbaz",
+ 4,
+ },
+ // Complete example:
+ {
+ " This is a list: \n\n\t* foo\n\t* bar\n\n\n\t* baz \nBAM ",
+ " This\nis a\nlist:\n\n\n *\nfoo\n *\nbar\n\n\n *\nbaz\nBAM\n",
+ 6,
+ },
+ }
+
+ for i, tc := range cases {
+ actual, lines := Wrap(tc.Input, tc.Lim)
+ if actual != tc.Output {
+ t.Fatalf("Case %d Input:\n\n`%s`\n\nExpected Output:\n\n`%s`\n\nActual Output:\n\n`%s`",
+ i, tc.Input, tc.Output, actual)
+ }
+
+ expected := len(strings.Split(tc.Output, "\n"))
+ if expected != lines {
+ t.Fatalf("Case %d Nb lines mismatch\nExpected:%d\nActual:%d",
+ i, expected, lines)
+ }
+ }
+}
+
+func TestWordLen(t *testing.T) {
+ cases := []struct {
+ Input string
+ Length int
+ }{
+ // A simple word
+ {
+ "foo",
+ 3,
+ },
+ // A simple word with colors
+ {
+ "\x1b[31mbar\x1b[0m",
+ 3,
+ },
+ // Handle prefix and suffix properly
+ {
+ "foo\x1b[31mfoobarHoy\x1b[0mbaaar",
+ 17,
+ },
+ }
+
+ for i, tc := range cases {
+ l := wordLen(tc.Input)
+ if l != tc.Length {
+ t.Fatalf("Case %d Input:\n\n`%s`\n\nExpected Output:\n\n`%d`\n\nActual Output:\n\n`%d`",
+ i, tc.Input, tc.Length, l)
+ }
+ }
+}
+
+func TestSplitWord(t *testing.T) {
+ cases := []struct {
+ Input string
+ Length int
+ Result, Leftover string
+ }{
+ // A simple word passes through.
+ {
+ "foo",
+ 4,
+ "foo", "",
+ },
+ // Cut at the right place
+ {
+ "foobarHoy",
+ 4,
+ "foob", "arHoy",
+ },
+ // A simple word passes through with colors
+ {
+ "\x1b[31mbar\x1b[0m",
+ 4,
+ "\x1b[31mbar\x1b[0m", "",
+ },
+ // Cut at the right place with colors
+ {
+ "\x1b[31mfoobarHoy\x1b[0m",
+ 4,
+ "\x1b[31mfoob", "arHoy\x1b[0m",
+ },
+ // Handle prefix and suffix properly
+ {
+ "foo\x1b[31mfoobarHoy\x1b[0mbaaar",
+ 4,
+ "foo\x1b[31mf", "oobarHoy\x1b[0mbaaar",
+ },
+ // Cut properly with length = 0
+ {
+ "foo",
+ 0,
+ "", "foo",
+ },
+ }
+
+ for i, tc := range cases {
+ result, leftover := splitWord(tc.Input, tc.Length)
+ if result != tc.Result || leftover != tc.Leftover {
+ t.Fatalf("Case %d Input:\n\n`%s`\n\nExpected Output:\n\n`%s` - `%s`\n\nActual Output:\n\n`%s` - `%s`",
+ i, tc.Input, tc.Result, tc.Leftover, result, leftover)
+ }
+ }
+}