Skip to content

Commit

Permalink
fix(ansi): cut/truncate/truncateleft improperly handling new lines (#367
Browse files Browse the repository at this point in the history
)
  • Loading branch information
caarlos0 authored Feb 11, 2025
1 parent e9b044f commit cd7b2ce
Show file tree
Hide file tree
Showing 2 changed files with 84 additions and 16 deletions.
41 changes: 29 additions & 12 deletions ansi/truncate.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,18 @@ import (

// Cut the string, without adding any prefix or tail strings. This function is
// aware of ANSI escape codes and will not break them, and accounts for
// wide-characters (such as East-Asian characters and emojis). Note that the
// [left] parameter is inclusive, while [right] isn't.
// wide-characters (such as East-Asian characters and emojis).
// This treats the text as a sequence of graphemes.
func Cut(s string, left, right int) string {
return cut(GraphemeWidth, s, left, right)
}

// CutWc the string, without adding any prefix or tail strings. This function is
// aware of ANSI escape codes and will not break them, and accounts for
// wide-characters (such as East-Asian characters and emojis). Note that the
// [left] parameter is inclusive, while [right] isn't.
// wide-characters (such as East-Asian characters and emojis).
// Note that the [left] parameter is inclusive, while [right] isn't,
// which is to say it'll return `[left, right)`.
//
// This treats the text as a sequence of wide characters and runes.
func CutWc(s string, left, right int) string {
return cut(WcWidth, s, left, right)
Expand All @@ -41,7 +42,7 @@ func cut(m Method, s string, left, right int) string {
if left == 0 {
return truncate(s, right, "")
}
return truncateLeft(Truncate(s, right, ""), left, "")
return truncateLeft(truncate(s, right, ""), left, "")
}

// Truncate truncates a string to a given length, adding a tail to the end if
Expand Down Expand Up @@ -99,6 +100,7 @@ func truncate(m Method, s string, length int, tail string) string {

// increment the index by the length of the cluster
i += len(cluster)
curWidth += width

// Are we ignoring? Skip to the next byte
if ignoring {
Expand All @@ -107,16 +109,15 @@ func truncate(m Method, s string, length int, tail string) string {

// Is this gonna be too wide?
// If so write the tail and stop collecting.
if curWidth+width > length && !ignoring {
if curWidth > length && !ignoring {
ignoring = true
buf.WriteString(tail)
}

if curWidth+width > length {
if curWidth > length {
continue
}

curWidth += width
buf.Write(cluster)

// Done collecting, now we're back in the ground state.
Expand All @@ -142,6 +143,14 @@ func truncate(m Method, s string, length int, tail string) string {
// collects printable ASCII
curWidth++
fallthrough
case parser.ExecuteAction:
// execute action will be things like \n, which, if outside the cut,
// should be ignored.
if ignoring {
i++
continue
}
fallthrough
default:
buf.WriteByte(b[i])
i++
Expand Down Expand Up @@ -214,14 +223,14 @@ func truncateLeft(m Method, s string, n int, prefix string) string {
buf.WriteString(prefix)
}

if ignoring {
continue
}

if curWidth > n {
buf.Write(cluster)
}

if ignoring {
continue
}

pstate = parser.GroundState
continue
}
Expand All @@ -240,6 +249,14 @@ func truncateLeft(m Method, s string, n int, prefix string) string {
continue
}

fallthrough
case parser.ExecuteAction:
// execute action will be things like \n, which, if outside the cut,
// should be ignored.
if ignoring {
i++
continue
}
fallthrough
default:
buf.WriteByte(b[i])
Expand Down
59 changes: 55 additions & 4 deletions ansi/truncate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ var tcases = []struct {
"on.",
".👋",
},
{
"simple multiple words",
"a couple of words",
"",
6,
"a coup",
"le of words",
},
{
"equalcontrolemoji",
"one\x1b[0m",
Expand Down Expand Up @@ -275,15 +283,53 @@ var tcases = []struct {
"…",
9,
"สวัสดีสวัสดี\x1b]8;;https://example.com\x1b\\\n\x1b]8;;\x1b\\",
"\x1b]8;;https://example.com\x1b\\\n…วัสดีสวัสดี\x1b]8;;\x1b\\",
"\x1b]8;;https://example.com\x1b\\…วัสดีสวัสดี\x1b]8;;\x1b\\",
},
{
"simple japanese text prefix/suffix",
"耐許ヱヨカハ調出あゆ監",
"…",
13,
"耐許ヱヨカハ…",
"…調出あゆ監",
},
{
"simple japanese text",
"耐許ヱヨカハ調出あゆ監",
"",
14,
"耐許ヱヨカハ調",
"出あゆ監",
},
{
"new line inside and outside range",
"\n\nsomething\nin\nthe\nway\n\n",
"-",
10,
"\n\nsomething\n-",
"-n\nthe\nway\n\n",
},
{
"multi-width graphemes with newlines - japanese text",
`耐許ヱヨカハ調出あゆ監件び理別よン國給災レホチ権輝モエフ会割もフ響3現エツ文時しだびほ経機ムイメフ敗文ヨク現義なさド請情ゆじょて憶主管州けでふく。排ゃわつげ美刊ヱミ出見ツ南者オ抜豆ハトロネ論索モネニイ任償スヲ話破リヤヨ秒止口イセソス止央のさ食周健でてつだ官送ト読聴遊容ひるべ。際ぐドらづ市居ネムヤ研校35岩6繹ごわク報拐イ革深52球ゃレスご究東スラ衝3間ラ録占たス。
禁にンご忘康ざほぎル騰般ねど事超スんいう真表何カモ自浩ヲシミ図客線るふ静王ぱーま写村月掛焼詐面ぞゃ。昇強ごントほ価保キ族85岡モテ恋困ひりこな刊並せご出来ぼぎむう点目ヲウ止環公ニレ事応タス必書タメムノ当84無信升ちひょ。価ーぐ中客テサ告覧ヨトハ極整
ラ得95稿はかラせ江利ス宏丸霊ミ考整ス静将ず業巨職ノラホ収嗅ざな。`,
"",
14,
"耐許ヱヨカハ調",
`出あゆ監件び理別よン國給災レホチ権輝モエフ会割もフ響3現エツ文時しだびほ経機ムイメフ敗文ヨク現義なさド請情ゆじょて憶主管州けでふく。排ゃわつげ美刊ヱミ出見ツ南者オ抜豆ハトロネ論索モネニイ任償スヲ話破リヤヨ秒止口イセソス止央のさ食周健でてつだ官送ト読聴遊容ひるべ。際ぐドらづ市居ネムヤ研校35岩6繹ごわク報拐イ革深52球ゃレスご究東スラ衝3間ラ録占たス。
禁にンご忘康ざほぎル騰般ねど事超スんいう真表何カモ自浩ヲシミ図客線るふ静王ぱーま写村月掛焼詐面ぞゃ。昇強ごントほ価保キ族85岡モテ恋困ひりこな刊並せご出来ぼぎむう点目ヲウ止環公ニレ事応タス必書タメムノ当84無信升ちひょ。価ーぐ中客テサ告覧ヨトハ極整
ラ得95稿はかラせ江利ス宏丸霊ミ考整ス静将ず業巨職ノラホ収嗅ざな。`,
},
}

func TestTruncate(t *testing.T) {
for i, c := range tcases {
t.Run(c.name, func(t *testing.T) {
if result := Truncate(c.input, c.width, c.extra); result != c.expectRight {
t.Errorf("test case %d failed: expected %q, got %q", i+1, c.expectRight, result)
t.Errorf("test case %d failed:\nexpected: %q\n got: %q", i+1, c.expectRight, result)
}
})
}
Expand All @@ -303,7 +349,7 @@ func TestTruncateLeft(t *testing.T) {
for i, c := range tcases {
t.Run(c.name, func(t *testing.T) {
if result := TruncateLeft(c.input, c.width, c.extra); result != c.expectLeft {
t.Errorf("test case %d failed: expected %q, got %q", i+1, c.expectLeft, result)
t.Errorf("test case %d failed:\nexpected: %q\n got: %q", i+1, c.expectLeft, result)
}
})
}
Expand Down Expand Up @@ -352,8 +398,13 @@ func TestCut(t *testing.T) {
"\x1b[38;5;212;48;5;63mHello, Artichoke!\x1b[m", 7, 16,
"\x1b[38;5;212;48;5;63mArtichoke\x1b[m",
},
{
"multiline",
"\n\x1b[38;2;98;98;98m\nif [ -f RE\nADME.md ]; then\x1b[m\n\x1b[38;2;98;98;98m echo oi\x1b[m\n\x1b[38;2;98;98;98mfi\x1b[m\n", 8, 13,
"\x1b[38;2;98;98;98mRE\nADM\x1b[m\x1b[38;2;98;98;98m\x1b[m\x1b[38;2;98;98;98m\x1b[m",
},
} {
t.Run(c.input, func(t *testing.T) {
t.Run(c.desc, func(t *testing.T) {
got := Cut(c.input, c.left, c.right)
if got != c.expect {
t.Errorf("%s (#%d):\nexpected: %q\ngot: %q", c.desc, i+1, c.expect, got)
Expand Down

0 comments on commit cd7b2ce

Please sign in to comment.