Files
rikuri/wrap.go
2026-04-20 21:38:22 +02:00

127 lines
3.4 KiB
Go

package wrap
import (
"strings"
"unicode"
"unicode/utf8"
)
type measuredString struct {
text string
unicodeSize int
}
func (str *measuredString) pushRune(text string, size int) string {
str.text += text[:size]
str.unicodeSize++
return text[size:]
}
func concat(a, b measuredString) measuredString {
return measuredString{
text: a.text + b.text,
unicodeSize: a.unicodeSize + b.unicodeSize,
}
}
// Given the space remaining on the current line as minWidth and the maximum line size as maxWidth,
// this function will read the first word and return three values:
// 1) the text that should go at the end of the current line,
// 2) the text that should go on the next line,
// 3) the remaining text that has not been consumed.
func wrapWord(text string, minWidth int, maxWidth int) (measuredString, measuredString, string) {
var space, prefix, suffix measuredString
reachedMin := false
reachedEnd := false
// read the first word, stopping at the end of the current line
for !reachedMin && len(text) > 0 {
rune, size := utf8.DecodeRuneInString(text)
if unicode.IsSpace(rune) {
if prefix.unicodeSize == 0 {
text = space.pushRune(text, size)
} else {
reachedMin = true
reachedEnd = true
}
} else {
if space.unicodeSize+prefix.unicodeSize < minWidth {
text = prefix.pushRune(text, size)
} else {
reachedMin = true
}
}
}
// does the entire word fit on the current line ?
if reachedEnd || len(text) == 0 {
return concat(space, prefix), measuredString{}, text
}
// otherwise finish reading the first word
for !reachedEnd && len(text) > 0 {
rune, size := utf8.DecodeRuneInString(text)
if unicode.IsSpace(rune) {
reachedEnd = true
} else {
if prefix.unicodeSize+suffix.unicodeSize < maxWidth {
text = suffix.pushRune(text, size)
} else {
// special case: if the word cannot fit on a single line,
// we put half the word on the current line and the other half on the next line
return concat(space, prefix), suffix, text
}
}
}
// and put the word on the next line
return measuredString{}, concat(prefix, suffix), text
}
// Break a string into lines of at most [width] runes.
func Wrap(text string, width int) (result []string) {
var line measuredString
for len(text) > 0 {
var cont, next measuredString
cont, next, text = wrapWord(text, width-line.unicodeSize, width)
line = concat(line, cont)
if next.unicodeSize > 0 {
result = append(result, line.text)
line = next
}
}
// add any text remaining in the buffer
return append(result, line.text)
}
// Prefix multiple lines of text with a string.
func Indent(prefix string, lines []string) []string {
result := make([]string, len(lines))
for i, line := range lines {
result[i] = prefix + line
}
return result
}
// Prefix multiple lines of text using a different string for the first line.
func Indentf(first string, prefix string, lines []string) []string {
result := make([]string, len(lines))
for i, line := range lines {
if i == 0 {
result[i] = first + line
} else {
result[i] = prefix + line
}
}
return result
}
// Prefix multiple lines of text with a number of spaces.
func Indents(size int, lines []string) []string {
return Indent(strings.Repeat(" ", size), lines)
}
// Prefix multiple lines of text with a number of spaces,
// but using a different string for the first line.
func Indentfs(first string, size int, lines []string) []string {
return Indentf(first, strings.Repeat(" ", size), lines)
}