From 460e895296120a2ffeb2213c5b5956386b2a3dd0 Mon Sep 17 00:00:00 2001 From: Naveen Mahalingam Date: Sun, 7 Jan 2024 22:30:37 -0800 Subject: [PATCH] table: process Carriage-Return codes correctly (#291) --- list/list.go | 4 +++- progress/render.go | 2 +- table/render_init.go | 2 +- table/render_test.go | 21 +++++++++++++++++++++ text/string.go | 24 ++++++++++++++++++++++++ text/string_test.go | 22 ++++++++++++++++++++++ 6 files changed, 72 insertions(+), 3 deletions(-) diff --git a/list/list.go b/list/list.go index 9f8cffe..91bda74 100644 --- a/list/list.go +++ b/list/list.go @@ -5,6 +5,8 @@ import ( "io" "strings" "unicode/utf8" + + "github.com/jedib0t/go-pretty/v6/text" ) const ( @@ -102,7 +104,7 @@ func (l *List) Style() *Style { func (l *List) analyzeAndStringify(item interface{}) *listItem { itemStr := fmt.Sprint(item) itemStr = strings.ReplaceAll(itemStr, "\t", " ") - itemStr = strings.ReplaceAll(itemStr, "\r", "") + itemStr = text.ProcessCRLF(itemStr) return &listItem{ Level: l.level, Text: itemStr, diff --git a/progress/render.go b/progress/render.go index 7ee4f15..697d8aa 100644 --- a/progress/render.go +++ b/progress/render.go @@ -195,7 +195,7 @@ func (p *Progress) renderPinnedMessages(out *strings.Builder) { func (p *Progress) renderTracker(out *strings.Builder, t *Tracker, hint renderHint) { message := t.message() message = strings.ReplaceAll(message, "\t", " ") - message = strings.ReplaceAll(message, "\r", "") + message = strings.ReplaceAll(message, "\r", "") // replace with text.ProcessCRLF? if p.lengthMessage > 0 { messageLen := text.RuneWidthWithoutEscSequences(message) if messageLen < p.lengthMessage { diff --git a/table/render_init.go b/table/render_init.go index 917879b..4aa0eea 100644 --- a/table/render_init.go +++ b/table/render_init.go @@ -44,7 +44,7 @@ func (t *Table) analyzeAndStringifyColumn(colIdx int, col interface{}, hint rend colStr = fmt.Sprint(col) } colStr = strings.ReplaceAll(colStr, "\t", " ") - colStr = strings.ReplaceAll(colStr, "\r", "") + colStr = text.ProcessCRLF(colStr) return fmt.Sprintf("%s%s", t.style.Format.Direction.Modifier(), colStr) } diff --git a/table/render_test.go b/table/render_test.go index e12f297..9dd9562 100644 --- a/table/render_test.go +++ b/table/render_test.go @@ -616,6 +616,27 @@ func TestTable_Render_ColumnConfigs(t *testing.T) { ) } +func TestTable_Render_CRLF(t *testing.T) { + tw := NewWriter() + tw.AppendHeader(testHeader) + tw.AppendRows(testRows) + tw.AppendRow(Row{5000, "Night", "King", 10000, "Was once a\r\nMortal \rMan"}) + tw.AppendFooter(testFooter) + + compareOutput(t, tw.Render(), ` ++------+------------+-----------+--------+-----------------------------+ +| # | FIRST NAME | LAST NAME | SALARY | | ++------+------------+-----------+--------+-----------------------------+ +| 1 | Arya | Stark | 3000 | | +| 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | +| 300 | Tyrion | Lannister | 5000 | | +| 5000 | Night | King | 10000 | Was once a | +| | | | | Man | ++------+------------+-----------+--------+-----------------------------+ +| | | TOTAL | 10000 | | ++------+------------+-----------+--------+-----------------------------+`) +} + func TestTable_Render_Empty(t *testing.T) { tw := NewWriter() assert.Empty(t, tw.Render()) diff --git a/text/string.go b/text/string.go index 9f5c9e8..a7b337a 100644 --- a/text/string.go +++ b/text/string.go @@ -1,6 +1,7 @@ package text import ( + "regexp" "strings" "unicode/utf8" @@ -107,6 +108,29 @@ func Pad(str string, maxLen int, paddingChar rune) string { return str } +var ( + reCarriageReturn = regexp.MustCompile(`(.*)\r`) +) + +// ProcessCRLF converts "\r\n" to "\n", and erases everything preceding a lone +// "\r" in each line of the string. +func ProcessCRLF(str string) string { + str = strings.ReplaceAll(str, "\r\n", "\n") + + // process \r by erasing everything preceding it in the line + if strings.Contains(str, "\r") { + lines := strings.Split(str, "\n") + for idx := range lines { + for reCarriageReturn.MatchString(lines[idx]) { + lines[idx] = reCarriageReturn.ReplaceAllString(lines[idx], "") + } + } + str = strings.Join(lines, "\n") + } + + return str +} + // RepeatAndTrim repeats the given string until it is as long as maxRunes. // For ex.: // diff --git a/text/string_test.go b/text/string_test.go index 96b1f17..42e1e0c 100644 --- a/text/string_test.go +++ b/text/string_test.go @@ -134,6 +134,28 @@ func TestPad(t *testing.T) { assert.Equal(t, "\x1b]8;;http://example.com\x1b\\Ghost\x1b]8;;\x1b\\.....", Pad("\x1b]8;;http://example.com\x1b\\Ghost\x1b]8;;\x1b\\", 10, '.')) } +func ExampleProcessCRLF() { + fmt.Printf("%#v\n", ProcessCRLF("abc")) + fmt.Printf("%#v\n", ProcessCRLF("abc\r\ndef")) + fmt.Printf("%#v\n", ProcessCRLF("abc\r\ndef\rghi")) + fmt.Printf("%#v\n", ProcessCRLF("abc\r\ndef\rghi\njkl")) + fmt.Printf("%#v\n", ProcessCRLF("abc\r\ndef\rghi\njkl\r")) + + // Output: "abc" + // "abc\ndef" + // "abc\nghi" + // "abc\nghi\njkl" + // "abc\nghi\n" +} + +func TestProcessCRLF(t *testing.T) { + assert.Equal(t, "abc", ProcessCRLF("abc")) + assert.Equal(t, "abc\ndef", ProcessCRLF("abc\r\ndef")) + assert.Equal(t, "abc\nghi", ProcessCRLF("abc\r\ndef\rghi")) + assert.Equal(t, "abc\nghi\njkl", ProcessCRLF("abc\r\ndef\rghi\njkl")) + assert.Equal(t, "abc\nghi\n", ProcessCRLF("abc\r\ndef\rghi\njkl\r")) +} + func ExampleRepeatAndTrim() { fmt.Printf("RepeatAndTrim(\"\", 5): %#v\n", RepeatAndTrim("", 5)) fmt.Printf("RepeatAndTrim(\"Ghost\", 0): %#v\n", RepeatAndTrim("Ghost", 0))