mirror of
https://github.com/charmbracelet/lipgloss.git
synced 2024-09-19 02:07:22 +03:00
272 lines
6.2 KiB
Go
272 lines
6.2 KiB
Go
package lipgloss
|
|
|
|
import (
|
|
"strings"
|
|
"unicode"
|
|
|
|
"github.com/muesli/reflow/indent"
|
|
"github.com/muesli/reflow/truncate"
|
|
"github.com/muesli/reflow/wordwrap"
|
|
"github.com/muesli/termenv"
|
|
)
|
|
|
|
// ANSI reset sequence.
|
|
const resetSeq = termenv.CSI + termenv.ResetSeq + "m"
|
|
|
|
// Style describes formatting instructions for a given string.
|
|
type Style struct {
|
|
Bold bool
|
|
Italic bool
|
|
Underline bool
|
|
Strikethrough bool
|
|
Reverse bool
|
|
Blink bool
|
|
Faint bool
|
|
Foreground ColorType
|
|
Background ColorType
|
|
|
|
// If the string contains multiple lines, they'll wrap at this value.
|
|
// Lines will also be padded with spaces so they'll all be the same width.
|
|
Width int
|
|
|
|
// Text alignment.
|
|
Align Align
|
|
|
|
// Padding. This will be colored according to the Background value if
|
|
// StylePadding is true.
|
|
LeftPadding int
|
|
RightPadding int
|
|
TopPadding int
|
|
BottomPadding int
|
|
|
|
// Whether or not to apply styling to the padding. Most notably, this
|
|
// determines whether or not the indent background color is styled.
|
|
StylePadding bool
|
|
|
|
// Margins. These will never be colored.
|
|
LeftMargin int
|
|
RightMargin int
|
|
TopMargin int
|
|
BottomMargin int
|
|
|
|
// If set, we truncate lines at this value after all other style has been
|
|
// applied. That is to say, the physical width of strings will not exceed
|
|
// this value, so this can be handy when building user interfaces.
|
|
MaxWidth int
|
|
|
|
// Whether or not to remove trailing spaces with no background color. By
|
|
// default we leave them in.
|
|
RenderClearTrailingSpaces bool
|
|
|
|
// Whether to draw underlines on spaces (like padding). We don't do this by
|
|
// default as it's likely not what people want, but you can turn it on if
|
|
// you so desire.
|
|
RenderUnderlinesOnSpaces bool
|
|
}
|
|
|
|
// Apply applies formatting to a given string.
|
|
func (s Style) Apply(str string) string {
|
|
return s.apply(str, false)
|
|
}
|
|
|
|
// Apply as applies formatting to the given string, removing newlines and
|
|
// skipping over block-level rules. Use this internally if you require an
|
|
// inline-style only in your library or component.
|
|
func (s Style) ApplyInline(str string) string {
|
|
return s.apply(str, true)
|
|
}
|
|
|
|
// WithMaxWidth applies a max width to a given style. This is useful in
|
|
// enforcing a certain width at render time, particularly with aribtrary
|
|
// strings and styles.
|
|
//
|
|
// Example:
|
|
//
|
|
// var userInput string = "..."
|
|
// var userStyle = text.Style{ /* ... */ }
|
|
// fmt.Println(userStyle.WithMaxWidth(16).Apply(userInput))
|
|
//
|
|
func (s Style) WithMaxWidth(n int) Style {
|
|
s.MaxWidth = n
|
|
return s
|
|
}
|
|
|
|
func (s Style) apply(str string, singleLine bool) string {
|
|
var (
|
|
styler = termenv.Style{}
|
|
|
|
// A copy of the main termenv styler, but without underlines. Used to
|
|
// not render underlines on spaces, if applicable. It's a pointer so
|
|
// we can treat it like a maybe monad, since it won't always be
|
|
// applicable.
|
|
noUnderlineStyler *termenv.Style
|
|
)
|
|
|
|
if s.Bold {
|
|
styler = styler.Bold()
|
|
}
|
|
if s.Italic {
|
|
styler = styler.Italic()
|
|
}
|
|
if s.Strikethrough {
|
|
styler = styler.CrossOut()
|
|
}
|
|
if s.Reverse {
|
|
styler = styler.Reverse()
|
|
}
|
|
if s.Blink {
|
|
styler = styler.Blink()
|
|
}
|
|
if s.Faint {
|
|
styler = styler.Faint()
|
|
}
|
|
|
|
switch c := s.Foreground.(type) {
|
|
case Color, AdaptiveColor:
|
|
styler = styler.Foreground(color(c.value()))
|
|
}
|
|
|
|
switch c := s.Background.(type) {
|
|
case Color, AdaptiveColor:
|
|
styler = styler.Background(color(c.value()))
|
|
}
|
|
|
|
if s.Underline {
|
|
if s.Underline && !s.RenderUnderlinesOnSpaces {
|
|
stylerCopy := styler
|
|
noUnderlineStyler = &stylerCopy
|
|
}
|
|
styler = styler.Underline()
|
|
}
|
|
|
|
// Strip spaces in single line mode
|
|
if singleLine {
|
|
str = strings.Replace(str, "\n", "", -1)
|
|
}
|
|
|
|
if !s.StylePadding {
|
|
str = styler.Styled(str)
|
|
}
|
|
|
|
// Word wrap
|
|
if !singleLine && s.Width > 0 {
|
|
str = wordwrap.String(str, s.Width-s.LeftPadding-s.RightPadding)
|
|
}
|
|
|
|
// Is a background color set?
|
|
backgroundColorSet := true
|
|
switch s.Background.(type) {
|
|
case nil, noColor:
|
|
backgroundColorSet = false
|
|
}
|
|
|
|
// Left/right padding
|
|
str = padLeft(str, s.LeftPadding)
|
|
if !s.RenderClearTrailingSpaces || backgroundColorSet {
|
|
str = padRight(str, s.RightPadding, s.StylePadding)
|
|
}
|
|
|
|
// Top/bottom padding
|
|
if !singleLine && s.TopPadding > 0 {
|
|
str = strings.Repeat("\n", s.TopPadding) + str
|
|
}
|
|
if !singleLine && s.BottomPadding > 0 {
|
|
str += strings.Repeat("\n", s.BottomPadding)
|
|
}
|
|
|
|
numLines := strings.Count(str, "\n")
|
|
|
|
// Set alignment. This will also pad short lines with spaces so that all
|
|
// lines are the same length.
|
|
if numLines > 0 && (!s.RenderClearTrailingSpaces || backgroundColorSet || s.Align != AlignLeft) {
|
|
str = align(str, s.Align)
|
|
}
|
|
|
|
if s.StylePadding {
|
|
// We have to do some extra work to not render underlines on spaces
|
|
if noUnderlineStyler != nil {
|
|
var b strings.Builder
|
|
|
|
for _, c := range str {
|
|
if unicode.IsSpace(c) {
|
|
b.WriteString(noUnderlineStyler.Styled(string(c)))
|
|
continue
|
|
}
|
|
b.WriteString(styler.Styled(string(c)))
|
|
}
|
|
|
|
str = b.String()
|
|
|
|
} else {
|
|
str = styler.Styled(str)
|
|
}
|
|
}
|
|
|
|
// Add left margin
|
|
str = padLeft(str, s.LeftMargin)
|
|
|
|
// Add right margin
|
|
if !s.RenderClearTrailingSpaces {
|
|
str = padRight(str, s.RightMargin, false)
|
|
}
|
|
|
|
// Top/bottom margin
|
|
if !singleLine {
|
|
var maybeSpaces string
|
|
|
|
if s.RenderClearTrailingSpaces {
|
|
_, width := getLines(str)
|
|
maybeSpaces = strings.Repeat(" ", width)
|
|
}
|
|
|
|
if s.TopMargin > 0 {
|
|
str = strings.Repeat(maybeSpaces+"\n", s.TopMargin) + str
|
|
}
|
|
if s.BottomMargin > 0 {
|
|
str += strings.Repeat("\n"+maybeSpaces, s.BottomMargin) + "\n"
|
|
}
|
|
}
|
|
|
|
// Truncate accoridng to MaxWidth
|
|
if s.MaxWidth > 0 {
|
|
lines := strings.Split(str, "\n")
|
|
|
|
for i := range lines {
|
|
lines[i] = truncate.String(lines[i], uint(s.MaxWidth))
|
|
}
|
|
|
|
str = strings.Join(lines, "\n")
|
|
}
|
|
|
|
return str
|
|
}
|
|
|
|
// Apply left padding.
|
|
func padLeft(str string, n int) string {
|
|
if n == 0 || str == "" {
|
|
return str
|
|
}
|
|
return indent.String(str, uint(n))
|
|
}
|
|
|
|
// Apply right right padding.
|
|
func padRight(str string, n int, stylePadding bool) string {
|
|
if n == 0 || str == "" {
|
|
return str
|
|
}
|
|
|
|
padding := strings.Repeat(" ", n)
|
|
|
|
var maybeReset string
|
|
if !stylePadding {
|
|
maybeReset = resetSeq
|
|
}
|
|
|
|
lines := strings.Split(str, "\n")
|
|
for i := range lines {
|
|
lines[i] += maybeReset + padding
|
|
}
|
|
|
|
return strings.Join(lines, "\n")
|
|
}
|