vendor/github.com/olekukonko/tablewriter/pkg/twwarp/wrap.go
// Copyright 2014 Oleku Konko All rights reserved.
// Use of this source code is governed by a MIT
// license that can be found in the LICENSE file.
// This module is a Table Writer API for the Go Programming Language.
// The protocols were written in pure Go and works on windows and unix systems
package twwarp
import (
"math"
"strings"
"unicode"
"github.com/clipperhouse/uax29/v2/graphemes"
"github.com/olekukonko/tablewriter/pkg/twwidth" // IMPORT YOUR NEW PACKAGE
// "github.com/mattn/go-runewidth" // This can be removed if all direct uses are gone
)
const (
nl = "\n"
sp = " "
)
const defaultPenalty = 1e5
func SplitWords(s string) []string {
words := make([]string, 0, len(s)/5)
var wordBegin int
wordPending := false
for i, c := range s {
if unicode.IsSpace(c) {
if wordPending {
words = append(words, s[wordBegin:i])
wordPending = false
}
continue
}
if !wordPending {
wordBegin = i
wordPending = true
}
}
if wordPending {
words = append(words, s[wordBegin:])
}
return words
}
// WrapString wraps s into a paragraph of lines of length lim, with minimal
// raggedness.
func WrapString(s string, lim int) ([]string, int) {
if s == sp {
return []string{sp}, lim
}
words := SplitWords(s)
if len(words) == 0 {
return []string{""}, lim
}
var lines []string
max := 0
for _, v := range words {
// max = runewidth.StringWidth(v) // OLD
max = twwidth.Width(v) // NEW: Use twdw.Width
if max > lim {
lim = max
}
}
for _, line := range WrapWords(words, 1, lim, defaultPenalty) {
lines = append(lines, strings.Join(line, sp))
}
return lines, lim
}
// WrapStringWithSpaces wraps a string into lines of a specified display width while preserving
// leading and trailing spaces. It splits the input string into words, condenses internal multiple
// spaces to a single space, and wraps the content to fit within the given width limit, measured
// using Unicode-aware display width. The function is used in the logging library to format log
// messages for consistent output. It returns the wrapped lines as a slice of strings and the
// adjusted width limit, which may increase if a single word exceeds the input limit. Thread-safe
// as it does not modify shared state.
func WrapStringWithSpaces(s string, lim int) ([]string, int) {
if len(s) == 0 {
return []string{""}, lim
}
if strings.TrimSpace(s) == "" { // All spaces
// if runewidth.StringWidth(s) <= lim { // OLD
if twwidth.Width(s) <= lim { // NEW: Use twdw.Width
// return []string{s}, runewidth.StringWidth(s) // OLD
return []string{s}, twwidth.Width(s) // NEW: Use twdw.Width
}
// For very long all-space strings, "wrap" by truncating to the limit.
if lim > 0 {
substring, _ := stringToDisplayWidth(s, lim)
return []string{substring}, lim
}
return []string{""}, lim
}
var leadingSpaces, trailingSpaces, coreContent string
firstNonSpace := strings.IndexFunc(s, func(r rune) bool { return !unicode.IsSpace(r) })
leadingSpaces = s[:firstNonSpace]
lastNonSpace := strings.LastIndexFunc(s, func(r rune) bool { return !unicode.IsSpace(r) })
trailingSpaces = s[lastNonSpace+1:]
coreContent = s[firstNonSpace : lastNonSpace+1]
if coreContent == "" {
return []string{leadingSpaces + trailingSpaces}, lim
}
words := SplitWords(coreContent)
if len(words) == 0 {
return []string{leadingSpaces + trailingSpaces}, lim
}
var lines []string
currentLim := lim
maxCoreWordWidth := 0
for _, v := range words {
// w := runewidth.StringWidth(v) // OLD
w := twwidth.Width(v) // NEW: Use twdw.Width
if w > maxCoreWordWidth {
maxCoreWordWidth = w
}
}
if maxCoreWordWidth > currentLim {
currentLim = maxCoreWordWidth
}
wrappedWordLines := WrapWords(words, 1, currentLim, defaultPenalty)
for i, lineWords := range wrappedWordLines {
joinedLine := strings.Join(lineWords, sp)
finalLine := leadingSpaces + joinedLine
if i == len(wrappedWordLines)-1 { // Last line
finalLine += trailingSpaces
}
lines = append(lines, finalLine)
}
return lines, currentLim
}
// stringToDisplayWidth returns a substring of s that has a display width
// as close as possible to, but not exceeding, targetWidth.
// It returns the substring and its actual display width.
func stringToDisplayWidth(s string, targetWidth int) (substring string, actualWidth int) {
if targetWidth <= 0 {
return "", 0
}
var currentWidth int
var endIndex int // Tracks the byte index in the original string
g := graphemes.FromString(s)
for g.Next() {
grapheme := g.Value()
// graphemeWidth := runewidth.StringWidth(grapheme) // OLD
graphemeWidth := twwidth.Width(grapheme) // NEW: Use twdw.Width
if currentWidth+graphemeWidth > targetWidth {
break
}
currentWidth += graphemeWidth
endIndex = g.End()
}
return s[:endIndex], currentWidth
}
// WrapWords is the low-level line-breaking algorithm, useful if you need more
// control over the details of the text wrapping process. For most uses,
// WrapString will be sufficient and more convenient.
//
// WrapWords splits a list of words into lines with minimal "raggedness",
// treating each rune as one unit, accounting for spc units between adjacent
// words on each line, and attempting to limit lines to lim units. Raggedness
// is the total error over all lines, where error is the square of the
// difference of the length of the line and lim. Too-long lines (which only
// happen when a single word is longer than lim units) have pen penalty units
// added to the error.
func WrapWords(words []string, spc, lim, pen int) [][]string {
n := len(words)
if n == 0 {
return nil
}
lengths := make([]int, n)
for i := 0; i < n; i++ {
// lengths[i] = runewidth.StringWidth(words[i]) // OLD
lengths[i] = twwidth.Width(words[i]) // NEW: Use twdw.Width
}
nbrk := make([]int, n)
cost := make([]int, n)
for i := range cost {
cost[i] = math.MaxInt32
}
remainderLen := lengths[n-1] // Uses updated lengths
for i := n - 1; i >= 0; i-- {
if i < n-1 {
remainderLen += spc + lengths[i]
}
if remainderLen <= lim {
cost[i] = 0
nbrk[i] = n
continue
}
phraseLen := lengths[i]
for j := i + 1; j < n; j++ {
if j > i+1 {
phraseLen += spc + lengths[j-1]
}
d := lim - phraseLen
c := d*d + cost[j]
if phraseLen > lim {
c += pen // too-long lines get a worse penalty
}
if c < cost[i] {
cost[i] = c
nbrk[i] = j
}
}
}
var lines [][]string
i := 0
for i < n {
lines = append(lines, words[i:nbrk[i]])
i = nbrk[i]
}
return lines
}
// getLines decomposes a multiline string into a slice of strings.
func getLines(s string) []string {
return strings.Split(s, nl)
}