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 examples, refer to the unit-tests or GoDoc examples.
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))
for idx, paragraph := range strings.Split(str, "\n\n") {
if idx > 0 {
out.WriteString("\n\n")
}
wrapHard(paragraph, wrapLen, 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 examples, refer to the unit-tests or GoDoc examples.
func WrapSoft(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))
for idx, paragraph := range strings.Split(str, "\n\n") {
if idx > 0 {
out.WriteString("\n\n")
}
wrapSoft(paragraph, wrapLen, out)
}
return out.String()
}
// WrapText is very similar to WrapHard except for one minor difference. Unlike
// WrapHard which discards line-breaks and respects only paragraph-breaks, this
// function respects line-breaks too.
//
// For examples, refer to the unit-tests or GoDoc examples.
func WrapText(str string, wrapLen int) string {
if wrapLen <= 0 {
return ""
}
var out strings.Builder
sLen := utf8.RuneCountInString(str)
out.Grow(sLen + (sLen / wrapLen))
lineIdx, isEscSeq, lastEscSeq := 0, false, ""
for _, char := range str {
if char == EscapeStartRune {
isEscSeq = true
lastEscSeq = ""
}
if isEscSeq {
lastEscSeq += string(char)
}
appendChar(char, wrapLen, &lineIdx, isEscSeq, lastEscSeq, &out)
if isEscSeq && char == EscapeStopRune {
isEscSeq = false
}
if lastEscSeq == EscapeReset {
lastEscSeq = ""
}
}
if lastEscSeq != "" && lastEscSeq != EscapeReset {
out.WriteString(EscapeReset)
}
return out.String()
}
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)
}
}
func wrapHard(paragraph string, wrapLen int, out *strings.Builder) {
lineLen, lastSeenEscSeq := 0, ""
words := strings.Fields(paragraph)
for wordIdx, word := range words {
escSeq := extractOpenEscapeSeq(word)
if escSeq != "" {
lastSeenEscSeq = escSeq
}
if lineLen > 0 {
out.WriteRune(' ')
lineLen++
}
wordLen := RuneCount(word)
if lineLen+wordLen <= wrapLen { // word fits within the line
out.WriteString(word)
lineLen += wordLen
} else { // word doesn't fit within the 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)
}
func wrapSoft(paragraph string, wrapLen int, out *strings.Builder) {
lineLen, lastSeenEscSeq := 0, ""
words := strings.Fields(paragraph)
for wordIdx, word := range words {
escSeq := extractOpenEscapeSeq(word)
if escSeq != "" {
lastSeenEscSeq = escSeq
}
spacing, spacingLen := "", 0
if lineLen > 0 {
spacing, spacingLen = " ", 1
}
wordLen := RuneCount(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)
}
|
The pages are generated with Golds v0.3.2. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @Go100and1 (reachable from the left QR code) to get the latest news of Golds. |