Initial QSfera import
This commit is contained in:
+229
@@ -0,0 +1,229 @@
|
||||
// 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"
|
||||
)
|
||||
|
||||
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 = twwidth.Width(v)
|
||||
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 twwidth.Width(s) <= lim {
|
||||
return []string{s}, twwidth.Width(s)
|
||||
}
|
||||
// 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 := twwidth.Width(v)
|
||||
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 := twwidth.Width(grapheme)
|
||||
|
||||
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] = twwidth.Width(words[i])
|
||||
}
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user