Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
text: refactor wrapping text into WrapHard/WrapSoft
- Loading branch information
Showing
5 changed files
with
302 additions
and
85 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,223 @@ | ||
package text | ||
|
||
import ( | ||
"strings" | ||
"unicode/utf8" | ||
) | ||
|
||
// WrapHard wraps a string to the given length using a newline. Handles strings | ||
// with ANSI escape sequences (such as text color) without breaking the text | ||
// formatting. Breaks all words that go beyond the line boundary. For ex.: | ||
// Wrap("Ghost", 1) == "G\nh\no\ns\nt" | ||
// Wrap("Ghost", 2) == "Gh\nos\nt" | ||
// Wrap("Ghost", 3) == "Gho\nst" | ||
// Wrap("Ghost", 4) == "Ghos\nt" | ||
// Wrap("Ghost", 5) == "Ghost" | ||
// Wrap("Ghost", 6) == "Ghost" | ||
// Wrap("Jon\nSnow", 2) == "Jo\nn\nSn\now" | ||
// Wrap("Jon\nSnow\n", 2) == "Jo\nn\nSn\now\n" | ||
// Wrap("Jon is a Snow", 5) == "Jon i\ns a S\nnow" | ||
// Wrap("\x1b[33mJon\x1b[0m\nSnow", 3) == "\x1b[33mJon\x1b[0m\nSno\nw" | ||
// Wrap("\x1b[33mJon Snow\x1b[0m", 3) == "\x1b[33mJon\x1b[0m\n\x1b[33m Sn\x1b[0m\n\x1b[33mow\x1b[0m" | ||
func WrapHard(str string, wrapLen int) string { | ||
if wrapLen <= 0 { | ||
return "" | ||
} | ||
|
||
str = strings.Replace(str, "\t", " ", -1) | ||
sLen := utf8.RuneCountInString(str) | ||
if sLen <= wrapLen { | ||
return str | ||
} | ||
|
||
out := &strings.Builder{} | ||
out.Grow(sLen + (sLen / wrapLen)) | ||
lineLen, inEscSeq, lastSeenEscSeq := 0, false, "" | ||
for _, char := range str { | ||
if char == EscapeStartRune { | ||
inEscSeq = true | ||
lastSeenEscSeq = "" | ||
} | ||
if inEscSeq { | ||
lastSeenEscSeq += string(char) | ||
} | ||
|
||
appendChar(char, wrapLen, &lineLen, inEscSeq, lastSeenEscSeq, out) | ||
|
||
if inEscSeq && char == EscapeStopRune { | ||
inEscSeq = false | ||
} | ||
if lastSeenEscSeq == EscapeReset { | ||
lastSeenEscSeq = "" | ||
} | ||
} | ||
terminateOutput(lastSeenEscSeq, out) | ||
|
||
return out.String() | ||
} | ||
|
||
// WrapSoft wraps a string to the given length using a newline. Handles strings | ||
// with ANSI escape sequences (such as text color) without breaking the text | ||
// formatting. Tries to move words that go beyond the line boundary to the next | ||
// line. For ex.: | ||
// Wrap("Ghost", 1) == "G\nh\no\ns\nt" | ||
// Wrap("Ghost", 2) == "Gh\nos\nt" | ||
// Wrap("Ghost", 3) == "Gho\nst" | ||
// Wrap("Ghost", 4) == "Ghos\nt" | ||
// Wrap("Ghost", 5) == "Ghost" | ||
// Wrap("Ghost", 6) == "Ghost" | ||
// Wrap("Jon\nSnow", 2) == "Jo\nn\nSn\now" | ||
// Wrap("Jon\nSnow\n", 2) == "Jo\nn\nSn\now\n" | ||
// Wrap("Jon is a Snow", 5) == "Jon \nis a \nSnow" | ||
// Wrap("\x1b[33mJon\x1b[0m\nSnow", 3) == "\x1b[33mJon\x1b[0m\nSno\nw" | ||
// Wrap("\x1b[33mJon Snow\x1b[0m", 3) == "\x1b[33mJon\x1b[0m\n\x1b[33mSno\x1b[0m\n\x1b[33mw\x1b[0m" | ||
func WrapSoft(str string, wrapLen int) string { | ||
if wrapLen <= 0 { | ||
return "" | ||
} | ||
sLen := utf8.RuneCountInString(str) | ||
if sLen <= wrapLen { | ||
return str | ||
} | ||
|
||
out := &strings.Builder{} | ||
out.Grow(sLen + (sLen / wrapLen)) | ||
lineLen, lastSeenEscSeq := 0, "" | ||
words := strings.Fields(str) | ||
for wordIdx, word := range words { | ||
escSeq := extractOpenEscapeSeq(word) | ||
if escSeq != "" { | ||
lastSeenEscSeq = escSeq | ||
} | ||
spacing, spacingLen := "", 0 | ||
if lineLen > 0 { | ||
spacing, spacingLen = " ", 1 | ||
} | ||
|
||
wordLen := RuneCountWithoutEscapeSeq(word) | ||
if lineLen+spacingLen+wordLen <= wrapLen { | ||
// word fits within the line | ||
out.WriteString(spacing) | ||
out.WriteString(word) | ||
lineLen += spacingLen + wordLen | ||
} else { | ||
// word doesn't fit within the line | ||
if lineLen > 0 { | ||
// something is already on the line; terminate it | ||
terminateLine(wrapLen, &lineLen, lastSeenEscSeq, out) | ||
} | ||
if wordLen <= wrapLen { | ||
// word fits within a single line | ||
out.WriteString(word) | ||
lineLen = wordLen | ||
} else { | ||
// word doesn't fit within a single line; hard-wrap | ||
appendWord(word, &lineLen, lastSeenEscSeq, wrapLen, out) | ||
} | ||
} | ||
|
||
// end of line; but more words incoming | ||
if lineLen == wrapLen && wordIdx < len(words)-1 { | ||
terminateLine(wrapLen, &lineLen, lastSeenEscSeq, out) | ||
} | ||
} | ||
terminateOutput(lastSeenEscSeq, out) | ||
|
||
return out.String() | ||
} | ||
|
||
// WrapText is an alias to WrapHard for backward-compatibility. This should be | ||
// removed in the next Major release. | ||
func WrapText(str string, wrapLen int) string { | ||
return WrapHard(str, wrapLen) | ||
} | ||
|
||
func appendChar(char rune, wrapLen int, lineLen *int, inEscSeq bool, lastSeenEscSeq string, out *strings.Builder) { | ||
// handle reaching the end of the line as dictated by wrapLen or by finding | ||
// a newline character | ||
if (*lineLen == wrapLen && !inEscSeq && char != '\n') || (char == '\n') { | ||
if lastSeenEscSeq != "" { | ||
// terminate escape sequence and the line; and restart the escape | ||
// sequence in the next line | ||
out.WriteString(EscapeReset) | ||
out.WriteRune('\n') | ||
out.WriteString(lastSeenEscSeq) | ||
} else { | ||
// just start a new line | ||
out.WriteRune('\n') | ||
} | ||
// reset line index to 0th character | ||
*lineLen = 0 | ||
} | ||
|
||
// if the rune is not a new line, output it | ||
if char != '\n' { | ||
out.WriteRune(char) | ||
|
||
// increment the line index if not in the middle of an escape sequence | ||
if !inEscSeq { | ||
*lineLen++ | ||
} | ||
} | ||
} | ||
|
||
func appendWord(word string, lineIdx *int, lastSeenEscSeq string, wrapLen int, out *strings.Builder) { | ||
inEscSeq := false | ||
for _, char := range word { | ||
if char == EscapeStartRune { | ||
inEscSeq = true | ||
lastSeenEscSeq = "" | ||
} | ||
if inEscSeq { | ||
lastSeenEscSeq += string(char) | ||
} | ||
|
||
appendChar(char, wrapLen, lineIdx, inEscSeq, lastSeenEscSeq, out) | ||
|
||
if inEscSeq && char == EscapeStopRune { | ||
inEscSeq = false | ||
} | ||
if lastSeenEscSeq == EscapeReset { | ||
lastSeenEscSeq = "" | ||
} | ||
} | ||
} | ||
|
||
func extractOpenEscapeSeq(str string) string { | ||
escapeSeq, inEscSeq := "", false | ||
for _, char := range str { | ||
if char == EscapeStartRune { | ||
inEscSeq = true | ||
escapeSeq = "" | ||
} | ||
if inEscSeq { | ||
escapeSeq += string(char) | ||
} | ||
if char == EscapeStopRune { | ||
inEscSeq = false | ||
} | ||
} | ||
if escapeSeq == EscapeReset { | ||
escapeSeq = "" | ||
} | ||
return escapeSeq | ||
} | ||
|
||
func terminateLine(wrapLen int, lineLen *int, lastSeenEscSeq string, out *strings.Builder) { | ||
if *lineLen < wrapLen { | ||
out.WriteString(strings.Repeat(" ", wrapLen-*lineLen)) | ||
} | ||
// something is already on the line; terminate it | ||
if lastSeenEscSeq != "" { | ||
out.WriteString(EscapeReset) | ||
} | ||
out.WriteRune('\n') | ||
out.WriteString(lastSeenEscSeq) | ||
*lineLen = 0 | ||
} | ||
|
||
func terminateOutput(lastSeenEscSeq string, out *strings.Builder) { | ||
if lastSeenEscSeq != "" && lastSeenEscSeq != EscapeReset && !strings.HasSuffix(out.String(), EscapeReset) { | ||
out.WriteString(EscapeReset) | ||
} | ||
} |
Oops, something went wrong.