main
Raw Download raw file
  1package cellbuf
  2
  3import (
  4	"bytes"
  5	"unicode"
  6	"unicode/utf8"
  7
  8	"github.com/charmbracelet/x/ansi"
  9)
 10
 11// Wrap returns a string that is wrapped to the specified limit applying any
 12// ANSI escape sequences in the string. It tries to wrap the string at word
 13// boundaries, but will break words if necessary.
 14//
 15// The breakpoints string is a list of characters that are considered
 16// breakpoints for word wrapping. A hyphen (-) is always considered a
 17// breakpoint.
 18//
 19// Note: breakpoints must be a string of 1-cell wide rune characters.
 20func Wrap(s string, limit int, breakpoints string) string {
 21	if len(s) == 0 {
 22		return ""
 23	}
 24
 25	if limit < 1 {
 26		return s
 27	}
 28
 29	p := ansi.GetParser()
 30	defer ansi.PutParser(p)
 31
 32	var (
 33		buf             bytes.Buffer
 34		word            bytes.Buffer
 35		space           bytes.Buffer
 36		style, curStyle Style
 37		link, curLink   Link
 38		curWidth        int
 39		wordLen         int
 40	)
 41
 42	addSpace := func() {
 43		curWidth += space.Len()
 44		buf.Write(space.Bytes())
 45		space.Reset()
 46	}
 47
 48	addWord := func() {
 49		if word.Len() == 0 {
 50			return
 51		}
 52
 53		curLink = link
 54		curStyle = style
 55
 56		addSpace()
 57		curWidth += wordLen
 58		buf.Write(word.Bytes())
 59		word.Reset()
 60		wordLen = 0
 61	}
 62
 63	addNewline := func() {
 64		if !curStyle.Empty() {
 65			buf.WriteString(ansi.ResetStyle)
 66		}
 67		if !curLink.Empty() {
 68			buf.WriteString(ansi.ResetHyperlink())
 69		}
 70		buf.WriteByte('\n')
 71		if !curLink.Empty() {
 72			buf.WriteString(ansi.SetHyperlink(curLink.URL, curLink.Params))
 73		}
 74		if !curStyle.Empty() {
 75			buf.WriteString(curStyle.Sequence())
 76		}
 77		curWidth = 0
 78		space.Reset()
 79	}
 80
 81	var state byte
 82	for len(s) > 0 {
 83		seq, width, n, newState := ansi.DecodeSequence(s, state, p)
 84		switch width {
 85		case 0:
 86			if ansi.Equal(seq, "\t") {
 87				addWord()
 88				space.WriteString(seq)
 89				break
 90			} else if ansi.Equal(seq, "\n") {
 91				if wordLen == 0 {
 92					if curWidth+space.Len() > limit {
 93						curWidth = 0
 94					} else {
 95						// preserve whitespaces
 96						buf.Write(space.Bytes())
 97					}
 98					space.Reset()
 99				}
100
101				addWord()
102				addNewline()
103				break
104			} else if ansi.HasCsiPrefix(seq) && p.Command() == 'm' {
105				// SGR style sequence [ansi.SGR]
106				ReadStyle(p.Params(), &style)
107			} else if ansi.HasOscPrefix(seq) && p.Command() == 8 {
108				// Hyperlink sequence [ansi.SetHyperlink]
109				ReadLink(p.Data(), &link)
110			}
111
112			word.WriteString(seq)
113		default:
114			if len(seq) == 1 {
115				// ASCII
116				r, _ := utf8.DecodeRuneInString(seq)
117				if unicode.IsSpace(r) {
118					addWord()
119					space.WriteRune(r)
120					break
121				} else if r == '-' || runeContainsAny(r, breakpoints) {
122					addSpace()
123					if curWidth+wordLen+width <= limit {
124						addWord()
125						buf.WriteString(seq)
126						curWidth += width
127						break
128					}
129				}
130			}
131
132			if wordLen+width > limit {
133				// Hardwrap the word if it's too long
134				addWord()
135			}
136
137			word.WriteString(seq)
138			wordLen += width
139
140			if curWidth+wordLen+space.Len() > limit {
141				addNewline()
142			}
143		}
144
145		s = s[n:]
146		state = newState
147	}
148
149	if wordLen == 0 {
150		if curWidth+space.Len() > limit {
151			curWidth = 0
152		} else {
153			// preserve whitespaces
154			buf.Write(space.Bytes())
155		}
156		space.Reset()
157	}
158
159	addWord()
160
161	if !curLink.Empty() {
162		buf.WriteString(ansi.ResetHyperlink())
163	}
164	if !curStyle.Empty() {
165		buf.WriteString(ansi.ResetStyle)
166	}
167
168	return buf.String()
169}
170
171func runeContainsAny[T string | []rune](r rune, s T) bool {
172	for _, c := range []rune(s) {
173		if c == r {
174			return true
175		}
176	}
177	return false
178}