decentral1se 31fa9b1a7a
Some checks failed
continuous-integration/drone/push Build is failing
chore: make deps, go mod vendor
2024-12-02 01:45:06 +01:00

401 lines
8.0 KiB
Go

package ansi
import (
"bytes"
"unicode"
"unicode/utf8"
"github.com/charmbracelet/x/ansi/parser"
"github.com/rivo/uniseg"
)
// nbsp is a non-breaking space
const nbsp = 0xA0
// Hardwrap wraps a string or a block of text to a given line length, breaking
// word boundaries. This will preserve ANSI escape codes and will account for
// wide-characters in the string.
// When preserveSpace is true, spaces at the beginning of a line will be
// preserved.
func Hardwrap(s string, limit int, preserveSpace bool) string {
if limit < 1 {
return s
}
var (
cluster []byte
buf bytes.Buffer
curWidth int
forceNewline bool
pstate = parser.GroundState // initial state
b = []byte(s)
)
addNewline := func() {
buf.WriteByte('\n')
curWidth = 0
}
i := 0
for i < len(b) {
state, action := parser.Table.Transition(pstate, b[i])
if state == parser.Utf8State {
var width int
cluster, _, width, _ = uniseg.FirstGraphemeCluster(b[i:], -1)
i += len(cluster)
if curWidth+width > limit {
addNewline()
}
if !preserveSpace && curWidth == 0 && len(cluster) <= 4 {
// Skip spaces at the beginning of a line
if r, _ := utf8.DecodeRune(cluster); r != utf8.RuneError && unicode.IsSpace(r) {
pstate = parser.GroundState
continue
}
}
buf.Write(cluster)
curWidth += width
pstate = parser.GroundState
continue
}
switch action {
case parser.PrintAction, parser.ExecuteAction:
if b[i] == '\n' {
addNewline()
forceNewline = false
break
}
if curWidth+1 > limit {
addNewline()
forceNewline = true
}
// Skip spaces at the beginning of a line
if curWidth == 0 {
if !preserveSpace && forceNewline && unicode.IsSpace(rune(b[i])) {
break
}
forceNewline = false
}
buf.WriteByte(b[i])
if action == parser.PrintAction {
curWidth++
}
default:
buf.WriteByte(b[i])
}
// We manage the UTF8 state separately manually above.
if pstate != parser.Utf8State {
pstate = state
}
i++
}
return buf.String()
}
// Wordwrap wraps a string or a block of text to a given line length, not
// breaking word boundaries. This will preserve ANSI escape codes and will
// account for wide-characters in the string.
// The breakpoints string is a list of characters that are considered
// breakpoints for word wrapping. A hyphen (-) is always considered a
// breakpoint.
//
// Note: breakpoints must be a string of 1-cell wide rune characters.
func Wordwrap(s string, limit int, breakpoints string) string {
if limit < 1 {
return s
}
var (
cluster []byte
buf bytes.Buffer
word bytes.Buffer
space bytes.Buffer
curWidth int
wordLen int
pstate = parser.GroundState // initial state
b = []byte(s)
)
addSpace := func() {
curWidth += space.Len()
buf.Write(space.Bytes())
space.Reset()
}
addWord := func() {
if word.Len() == 0 {
return
}
addSpace()
curWidth += wordLen
buf.Write(word.Bytes())
word.Reset()
wordLen = 0
}
addNewline := func() {
buf.WriteByte('\n')
curWidth = 0
space.Reset()
}
i := 0
for i < len(b) {
state, action := parser.Table.Transition(pstate, b[i])
if state == parser.Utf8State {
var width int
cluster, _, width, _ = uniseg.FirstGraphemeCluster(b[i:], -1)
i += len(cluster)
r, _ := utf8.DecodeRune(cluster)
if r != utf8.RuneError && unicode.IsSpace(r) && r != nbsp {
addWord()
space.WriteRune(r)
} else if bytes.ContainsAny(cluster, breakpoints) {
addSpace()
addWord()
buf.Write(cluster)
curWidth++
} else {
word.Write(cluster)
wordLen += width
if curWidth+space.Len()+wordLen > limit &&
wordLen < limit {
addNewline()
}
}
pstate = parser.GroundState
continue
}
switch action {
case parser.PrintAction, parser.ExecuteAction:
r := rune(b[i])
switch {
case r == '\n':
if wordLen == 0 {
if curWidth+space.Len() > limit {
curWidth = 0
} else {
buf.Write(space.Bytes())
}
space.Reset()
}
addWord()
addNewline()
case unicode.IsSpace(r):
addWord()
space.WriteByte(b[i])
case r == '-':
fallthrough
case runeContainsAny(r, breakpoints):
addSpace()
addWord()
buf.WriteByte(b[i])
curWidth++
default:
word.WriteByte(b[i])
wordLen++
if curWidth+space.Len()+wordLen > limit &&
wordLen < limit {
addNewline()
}
}
default:
word.WriteByte(b[i])
}
// We manage the UTF8 state separately manually above.
if pstate != parser.Utf8State {
pstate = state
}
i++
}
addWord()
return buf.String()
}
// Wrap wraps a string or a block of text to a given line length, breaking word
// boundaries if necessary. This will preserve ANSI escape codes and will
// account for wide-characters in the string. The breakpoints string is a list
// of characters that are considered breakpoints for word wrapping. A hyphen
// (-) is always considered a breakpoint.
//
// Note: breakpoints must be a string of 1-cell wide rune characters.
func Wrap(s string, limit int, breakpoints string) string {
if limit < 1 {
return s
}
var (
cluster []byte
buf bytes.Buffer
word bytes.Buffer
space bytes.Buffer
curWidth int // written width of the line
wordLen int // word buffer len without ANSI escape codes
pstate = parser.GroundState // initial state
b = []byte(s)
)
addSpace := func() {
curWidth += space.Len()
buf.Write(space.Bytes())
space.Reset()
}
addWord := func() {
if word.Len() == 0 {
return
}
addSpace()
curWidth += wordLen
buf.Write(word.Bytes())
word.Reset()
wordLen = 0
}
addNewline := func() {
buf.WriteByte('\n')
curWidth = 0
space.Reset()
}
i := 0
for i < len(b) {
state, action := parser.Table.Transition(pstate, b[i])
if state == parser.Utf8State {
var width int
cluster, _, width, _ = uniseg.FirstGraphemeCluster(b[i:], -1)
i += len(cluster)
r, _ := utf8.DecodeRune(cluster)
switch {
case r != utf8.RuneError && unicode.IsSpace(r) && r != nbsp: // nbsp is a non-breaking space
addWord()
space.WriteRune(r)
case bytes.ContainsAny(cluster, breakpoints):
addSpace()
if curWidth+wordLen+width > limit {
word.Write(cluster)
wordLen += width
} else {
addWord()
buf.Write(cluster)
curWidth += width
}
default:
if wordLen+width > limit {
// Hardwrap the word if it's too long
addWord()
}
word.Write(cluster)
wordLen += width
if curWidth+wordLen+space.Len() > limit {
addNewline()
}
}
pstate = parser.GroundState
continue
}
switch action {
case parser.PrintAction, parser.ExecuteAction:
switch r := rune(b[i]); {
case r == '\n':
if wordLen == 0 {
if curWidth+space.Len() > limit {
curWidth = 0
} else {
// preserve whitespaces
buf.Write(space.Bytes())
}
space.Reset()
}
addWord()
addNewline()
case unicode.IsSpace(r):
addWord()
space.WriteRune(r)
case r == '-':
fallthrough
case runeContainsAny(r, breakpoints):
addSpace()
if curWidth+wordLen >= limit {
// We can't fit the breakpoint in the current line, treat
// it as part of the word.
word.WriteRune(r)
wordLen++
} else {
addWord()
buf.WriteRune(r)
curWidth++
}
default:
if curWidth == limit {
addNewline()
}
word.WriteRune(r)
wordLen++
if wordLen == limit {
// Hardwrap the word if it's too long
addWord()
}
if curWidth+wordLen+space.Len() > limit {
addNewline()
}
}
default:
word.WriteByte(b[i])
}
// We manage the UTF8 state separately manually above.
if pstate != parser.Utf8State {
pstate = state
}
i++
}
if word.Len() != 0 {
// Preserve ANSI wrapped spaces at the end of string
if curWidth+space.Len() > limit {
buf.WriteByte('\n')
}
addSpace()
}
buf.Write(word.Bytes())
return buf.String()
}
func runeContainsAny(r rune, s string) bool {
for _, c := range s {
if c == r {
return true
}
}
return false
}