From f72a9dc62ba20546b2cdeb466434fc1900741a4f Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sun, 3 Nov 2019 14:00:35 +0100 Subject: switch to go-term-text to fix bad underflow for label rendering --- vendor/github.com/MichaelMure/go-term-text/wrap.go | 334 +++++++++++++++++++++ 1 file changed, 334 insertions(+) create mode 100644 vendor/github.com/MichaelMure/go-term-text/wrap.go (limited to 'vendor/github.com/MichaelMure/go-term-text/wrap.go') diff --git a/vendor/github.com/MichaelMure/go-term-text/wrap.go b/vendor/github.com/MichaelMure/go-term-text/wrap.go new file mode 100644 index 00000000..2fd6ed5f --- /dev/null +++ b/vendor/github.com/MichaelMure/go-term-text/wrap.go @@ -0,0 +1,334 @@ +package text + +import ( + "strings" + + "github.com/mattn/go-runewidth" +) + +// Force runewidth not to treat ambiguous runes as wide chars, so that things +// like unicode ellipsis/up/down/left/right glyphs can have correct runewidth +// and can be displayed correctly in terminals. +func init() { + runewidth.DefaultCondition.EastAsianWidth = false +} + +// Wrap a text for a given line size. +// Handle properly terminal color escape code +func Wrap(text string, lineWidth int) (string, int) { + return WrapLeftPadded(text, lineWidth, 0) +} + +// WrapLeftPadded wrap a text for a given line size with a left padding. +// Handle properly terminal color escape code +func WrapLeftPadded(text string, lineWidth int, leftPad int) (string, int) { + pad := strings.Repeat(" ", leftPad) + return WrapWithPad(text, lineWidth, pad) +} + +// WrapWithPad wrap a text for a given line size with a custom left padding +// Handle properly terminal color escape code +func WrapWithPad(text string, lineWidth int, pad string) (string, int) { + return WrapWithPadIndent(text, lineWidth, pad, pad) +} + +// WrapWithPad wrap a text for a given line size with a custom left padding +// This function also align the result depending on the requested alignment. +// Handle properly terminal color escape code +func WrapWithPadAlign(text string, lineWidth int, pad string, align Alignment) (string, int) { + return WrapWithPadIndentAlign(text, lineWidth, pad, pad, align) +} + +// WrapWithPadIndent wrap a text for a given line size with a custom left padding +// and a first line indent. The padding is not effective on the first line, indent +// is used instead, which allow to implement indents and outdents. +// Handle properly terminal color escape code +func WrapWithPadIndent(text string, lineWidth int, indent string, pad string) (string, int) { + return WrapWithPadIndentAlign(text, lineWidth, indent, pad, NoAlign) +} + +// WrapWithPadIndentAlign wrap a text for a given line size with a custom left padding +// and a first line indent. The padding is not effective on the first line, indent +// is used instead, which allow to implement indents and outdents. +// This function also align the result depending on the requested alignment. +// Handle properly terminal color escape code +func WrapWithPadIndentAlign(text string, lineWidth int, indent string, pad string, align Alignment) (string, int) { + var lines []string + nbLine := 0 + + // Start with the indent + padStr := indent + padLen := Len(indent) + + // tabs are formatted as 4 spaces + text = strings.Replace(text, "\t", " ", -1) + + // NOTE: text is first segmented into lines so that softwrapLine can handle. + for i, line := range strings.Split(text, "\n") { + // on the second line, use the padding instead + if i == 1 { + padStr = pad + padLen = Len(pad) + } + + if line == "" || strings.TrimSpace(line) == "" { + // nothing in the line, we just add the non-empty part of the padding + lines = append(lines, strings.TrimRight(padStr, " ")) + nbLine++ + continue + } + + wrapped := softwrapLine(line, lineWidth-padLen) + split := strings.Split(wrapped, "\n") + + if i == 0 && len(split) > 1 { + // the very first line got wrapped + // that means we need to switch to the normal padding + // use the first wrapped line, ignore everything else and + // wrap the remaining of the line with the normal padding. + + content := LineAlign(strings.TrimRight(split[0], " "), lineWidth-padLen, align) + lines = append(lines, padStr+content) + nbLine++ + line = strings.TrimPrefix(line, split[0]) + line = strings.TrimLeft(line, " ") + + padStr = pad + padLen = Len(pad) + wrapped = softwrapLine(line, lineWidth-padLen) + split = strings.Split(wrapped, "\n") + } + + for j, seg := range split { + if j == 0 { + // keep the left padding of the wrapped line + content := LineAlign(strings.TrimRight(seg, " "), lineWidth-padLen, align) + lines = append(lines, padStr+content) + } else { + content := LineAlign(strings.TrimSpace(seg), lineWidth-padLen, align) + lines = append(lines, padStr+content) + } + nbLine++ + } + } + + return strings.Join(lines, "\n"), nbLine +} + +// Break a line into several lines so that each line consumes at most +// 'textWidth' cells. Lines break at groups of white spaces and multibyte +// chars. Nothing is removed from the original text so that it behaves like a +// softwrap. +// +// Required: The line shall not contain '\n' +// +// WRAPPING ALGORITHM: The line is broken into non-breakable chunks, then line +// breaks ("\n") are inserted between these groups so that the total length +// between breaks does not exceed the required width. Words that are longer than +// the textWidth are broken into pieces no longer than textWidth. +func softwrapLine(line string, textWidth int) string { + escaped, escapes := ExtractTermEscapes(line) + + chunks := segmentLine(escaped) + // Reverse the chunk array so we can use it as a stack. + for i, j := 0, len(chunks)-1; i < j; i, j = i+1, j-1 { + chunks[i], chunks[j] = chunks[j], chunks[i] + } + + // for readability, minimal implementation of a stack: + + pop := func() string { + result := chunks[len(chunks)-1] + chunks = chunks[:len(chunks)-1] + return result + } + + push := func(chunk string) { + chunks = append(chunks, chunk) + } + + peek := func() string { + return chunks[len(chunks)-1] + } + + empty := func() bool { + return len(chunks) == 0 + } + + var out strings.Builder + + // helper to write in the output while interleaving the escape + // sequence at the correct places. + // note: the final algorithm will add additional line break in the original + // text. Those line break are *not* fed to this helper so the positions don't + // need to be offset, which make the whole thing much easier. + currPos := 0 + currItem := 0 + outputString := func(s string) { + for _, r := range s { + for currItem < len(escapes) && currPos == escapes[currItem].Pos { + out.WriteString(escapes[currItem].Item) + currItem++ + } + out.WriteRune(r) + currPos++ + } + } + + width := 0 + + for !empty() { + wl := Len(peek()) + + if width+wl <= textWidth { + // the chunk fit in the available space + outputString(pop()) + width += wl + if width == textWidth && !empty() { + // only add line break when there is more chunk to come + out.WriteRune('\n') + width = 0 + } + } else if wl > textWidth { + // words too long for a full line are split to fill the remaining space. + // But if the long words is the first non-space word in the middle of the + // line, preceding spaces shall not be counted in word splitting. + splitWidth := textWidth - width + if strings.HasSuffix(out.String(), "\n"+strings.Repeat(" ", width)) { + splitWidth += width + } + left, right := splitWord(pop(), splitWidth) + // remainder is pushed back to the stack for next round + push(right) + outputString(left) + out.WriteRune('\n') + width = 0 + } else { + // normal line overflow, we add a line break and try again + out.WriteRune('\n') + width = 0 + } + } + + // Don't forget the trailing escapes, if any. + for currItem < len(escapes) && currPos >= escapes[currItem].Pos { + out.WriteString(escapes[currItem].Item) + currItem++ + } + + return out.String() +} + +// Segment a line into chunks, where each chunk consists of chars with the same +// type and is not breakable. +func segmentLine(s string) []string { + var chunks []string + + var word string + wordType := none + flushWord := func() { + chunks = append(chunks, word) + word = "" + wordType = none + } + + for _, r := range s { + // A WIDE_CHAR itself constitutes a chunk. + thisType := runeType(r) + if thisType == wideChar { + if wordType != none { + flushWord() + } + chunks = append(chunks, string(r)) + continue + } + // Other type of chunks starts with a char of that type, and ends with a + // char with different type or end of string. + if thisType != wordType { + if wordType != none { + flushWord() + } + word = string(r) + wordType = thisType + } else { + word += string(r) + } + } + if word != "" { + flushWord() + } + + return chunks +} + +type RuneType int + +// Rune categories +// +// These categories are so defined that each category forms a non-breakable +// chunk. It IS NOT the same as unicode code point categories. +const ( + none RuneType = iota + wideChar + invisible + shortUnicode + space + visibleAscii +) + +// Determine the category of a rune. +func runeType(r rune) RuneType { + rw := runewidth.RuneWidth(r) + if rw > 1 { + return wideChar + } else if rw == 0 { + return invisible + } else if r > 127 { + return shortUnicode + } else if r == ' ' { + return space + } else { + return visibleAscii + } +} + +// splitWord split a word at the given length, while ignoring the terminal escape sequences +func splitWord(word string, length int) (string, string) { + runes := []rune(word) + var result []rune + added := 0 + escape := false + + if length == 0 { + return "", word + } + + for _, r := range runes { + if r == '\x1b' { + escape = true + } + + width := runewidth.RuneWidth(r) + if width+added > length { + // wide character made the length overflow + break + } + + result = append(result, r) + + if !escape { + added += width + if added >= length { + break + } + } + + if r == 'm' { + escape = false + } + } + + leftover := runes[len(result):] + + return string(result), string(leftover) +} -- cgit