2021-03-02 02:29:00 +03:00
|
|
|
package lipgloss
|
|
|
|
|
|
|
|
import (
|
|
|
|
"strings"
|
2021-03-06 00:34:46 +03:00
|
|
|
"unicode"
|
2021-03-02 02:29:00 +03:00
|
|
|
|
|
|
|
"github.com/muesli/reflow/truncate"
|
|
|
|
"github.com/muesli/reflow/wordwrap"
|
|
|
|
"github.com/muesli/termenv"
|
|
|
|
)
|
|
|
|
|
2021-03-16 20:42:24 +03:00
|
|
|
// Property for a key.
|
2021-03-08 15:59:32 +03:00
|
|
|
type propKey int
|
|
|
|
|
2021-03-16 20:42:24 +03:00
|
|
|
// Available properties.
|
2021-03-08 15:59:32 +03:00
|
|
|
const (
|
|
|
|
boldKey propKey = iota
|
|
|
|
italicKey
|
|
|
|
underlineKey
|
|
|
|
strikethroughKey
|
|
|
|
reverseKey
|
|
|
|
blinkKey
|
|
|
|
faintKey
|
|
|
|
foregroundKey
|
|
|
|
backgroundKey
|
|
|
|
widthKey
|
|
|
|
alignKey
|
|
|
|
topPaddingKey
|
|
|
|
rightPaddingKey
|
|
|
|
bottomPaddingKey
|
|
|
|
leftPaddingKey
|
|
|
|
colorWhitespaceKey
|
|
|
|
topMarginKey
|
|
|
|
rightMarginKey
|
|
|
|
bottomMarginKey
|
|
|
|
leftMarginKey
|
|
|
|
inlineKey
|
|
|
|
maxWidthKey
|
|
|
|
drawClearTrailingSpacesKey
|
|
|
|
underlineWhitespaceKey
|
|
|
|
strikethroughWhitespaceKey
|
|
|
|
underlineSpacesKey
|
|
|
|
strikethroughSpacesKey
|
|
|
|
)
|
2021-03-04 20:54:40 +03:00
|
|
|
|
2021-03-16 20:42:24 +03:00
|
|
|
// A set of properties.
|
|
|
|
type rules map[propKey]interface{}
|
|
|
|
|
|
|
|
// NewStyle returns a new, empty Style. While it's syntactic sugar for the
|
2021-03-18 18:23:08 +03:00
|
|
|
// Style{} primitive, it's recommended to use this function for creating styles
|
2021-03-18 03:29:18 +03:00
|
|
|
// incase the underlying implementation changes.
|
2021-03-05 03:23:41 +03:00
|
|
|
func NewStyle() Style {
|
2021-03-16 20:42:24 +03:00
|
|
|
return Style{}
|
2021-03-05 03:23:41 +03:00
|
|
|
}
|
|
|
|
|
2021-03-18 03:29:18 +03:00
|
|
|
// Style contains a set of rules that comprise a style as a whole.
|
2021-03-16 20:42:24 +03:00
|
|
|
type Style struct {
|
|
|
|
rules map[propKey]interface{}
|
|
|
|
value string
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetString sets the underlying string value for this style. To render once
|
|
|
|
// the underlying string is set, use the Style.String. This method is
|
|
|
|
// a convenience for cases when having a stringer implementation is handy, such
|
|
|
|
// as when using fmt.Sprintf. You can also simply define a style and render out
|
|
|
|
// strings directly with Style.Render.
|
|
|
|
func (s Style) SetString(str string) Style {
|
|
|
|
s.value = str
|
|
|
|
return s
|
|
|
|
}
|
|
|
|
|
|
|
|
// String implements stringer for a Style, returning the rendered result based
|
|
|
|
// on the rules in this style. An underlying string value must be set with
|
|
|
|
// Style.SetString prior to using this method.
|
|
|
|
func (s Style) String() string {
|
|
|
|
return s.Render(s.value)
|
|
|
|
}
|
2021-03-05 03:23:41 +03:00
|
|
|
|
2021-03-24 19:58:56 +03:00
|
|
|
// Copy returns a copy of this style, including any underlying string values.
|
2021-03-08 15:59:32 +03:00
|
|
|
func (s Style) Copy() Style {
|
2021-03-16 20:42:24 +03:00
|
|
|
o := NewStyle()
|
2021-03-16 23:35:28 +03:00
|
|
|
o.rules = make(rules)
|
2021-03-16 20:42:24 +03:00
|
|
|
for k, v := range s.rules {
|
|
|
|
o.rules[k] = v
|
2021-03-05 03:23:41 +03:00
|
|
|
}
|
2021-03-24 19:58:56 +03:00
|
|
|
o.value = s.value
|
2021-03-05 18:04:58 +03:00
|
|
|
return o
|
2021-03-05 03:23:41 +03:00
|
|
|
}
|
|
|
|
|
2021-03-08 15:59:32 +03:00
|
|
|
// Inherit takes values from the style in the argument applies them to this
|
|
|
|
// style, overwriting existing definitions. Only values explicitly set on the
|
|
|
|
// style in argument will be applied.
|
|
|
|
//
|
2021-03-18 20:29:48 +03:00
|
|
|
// Margins, padding, and underlying string values are not inherited.
|
2021-03-08 15:59:32 +03:00
|
|
|
func (o Style) Inherit(i Style) {
|
2021-03-16 20:42:24 +03:00
|
|
|
for k, v := range i.rules {
|
2021-03-08 15:59:32 +03:00
|
|
|
switch k {
|
|
|
|
case topMarginKey, rightMarginKey, bottomMarginKey, leftMarginKey:
|
|
|
|
// Margins are not inherited
|
|
|
|
continue
|
|
|
|
case topPaddingKey, rightPaddingKey, bottomPaddingKey, leftPaddingKey:
|
|
|
|
// Padding is not inherited
|
|
|
|
continue
|
|
|
|
}
|
2021-03-06 03:25:58 +03:00
|
|
|
|
2021-03-16 20:42:24 +03:00
|
|
|
if _, exists := o.rules[k]; exists {
|
2021-03-08 15:59:32 +03:00
|
|
|
continue
|
|
|
|
}
|
2021-03-16 20:42:24 +03:00
|
|
|
o.rules[k] = v
|
2021-03-06 03:25:58 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-08 15:59:32 +03:00
|
|
|
// Render applies the defined style formatting to a given string.
|
2021-03-05 05:07:54 +03:00
|
|
|
func (s Style) Render(str string) string {
|
2021-03-02 02:29:00 +03:00
|
|
|
var (
|
2021-03-08 15:59:32 +03:00
|
|
|
te termenv.Style
|
|
|
|
teSpace termenv.Style
|
|
|
|
teWhitespace termenv.Style
|
|
|
|
|
|
|
|
bold = s.getAsBool(boldKey, false)
|
|
|
|
italic = s.getAsBool(italicKey, false)
|
|
|
|
underline = s.getAsBool(underlineKey, false)
|
|
|
|
strikethrough = s.getAsBool(strikethroughKey, false)
|
|
|
|
reverse = s.getAsBool(reverseKey, false)
|
|
|
|
blink = s.getAsBool(blinkKey, false)
|
|
|
|
faint = s.getAsBool(faintKey, false)
|
|
|
|
|
|
|
|
fg = s.getAsColor(foregroundKey)
|
|
|
|
bg = s.getAsColor(backgroundKey)
|
|
|
|
|
|
|
|
width = s.getAsInt(widthKey)
|
|
|
|
align = s.getAsAlign(alignKey)
|
|
|
|
|
|
|
|
topPadding = s.getAsInt(topPaddingKey)
|
|
|
|
rightPadding = s.getAsInt(rightPaddingKey)
|
|
|
|
bottomPadding = s.getAsInt(bottomPaddingKey)
|
|
|
|
leftPadding = s.getAsInt(leftPaddingKey)
|
|
|
|
|
|
|
|
topMargin = s.getAsInt(topMarginKey)
|
|
|
|
rightMargin = s.getAsInt(rightMarginKey)
|
|
|
|
bottomMargin = s.getAsInt(bottomMarginKey)
|
|
|
|
leftMargin = s.getAsInt(leftMarginKey)
|
|
|
|
|
|
|
|
colorWhitespace = s.getAsBool(colorWhitespaceKey, true)
|
|
|
|
inline = s.getAsBool(inlineKey, false)
|
|
|
|
maxWidth = s.getAsInt(maxWidthKey)
|
|
|
|
|
|
|
|
drawClearTrailingSpaces = s.getAsBool(drawClearTrailingSpacesKey, true)
|
|
|
|
underlineWhitespace = s.getAsBool(underlineWhitespaceKey, false)
|
|
|
|
strikethroughWhitespace = s.getAsBool(strikethroughWhitespaceKey, false)
|
|
|
|
|
|
|
|
underlineSpaces = underline && s.getAsBool(underlineSpacesKey, true)
|
|
|
|
strikethroughSpaces = strikethrough && s.getAsBool(strikethroughSpacesKey, true)
|
|
|
|
|
|
|
|
// Do we need to style whitespace (padding and space outsode
|
|
|
|
// paragraphs) separately?
|
|
|
|
styleWhitespace = underlineWhitespace || strikethroughWhitespace
|
|
|
|
|
|
|
|
// Do we need to style spaces separately?
|
|
|
|
useSpaceStyler = underlineSpaces || strikethroughSpaces
|
2021-03-05 20:23:25 +03:00
|
|
|
)
|
2021-03-05 05:07:54 +03:00
|
|
|
|
2021-03-08 15:59:32 +03:00
|
|
|
if bold {
|
|
|
|
te = te.Bold()
|
2021-03-05 20:23:25 +03:00
|
|
|
}
|
2021-03-08 15:59:32 +03:00
|
|
|
if italic {
|
|
|
|
te = te.Italic()
|
2021-03-02 02:29:00 +03:00
|
|
|
}
|
2021-03-08 15:59:32 +03:00
|
|
|
if underline {
|
|
|
|
te = te.Underline()
|
2021-03-02 02:29:00 +03:00
|
|
|
}
|
2021-03-08 15:59:32 +03:00
|
|
|
if reverse {
|
|
|
|
te = te.Reverse()
|
2021-03-02 02:29:00 +03:00
|
|
|
}
|
2021-03-08 15:59:32 +03:00
|
|
|
if blink {
|
|
|
|
te = te.Blink()
|
2021-03-02 02:29:00 +03:00
|
|
|
}
|
2021-03-08 15:59:32 +03:00
|
|
|
if faint {
|
|
|
|
te = te.Faint()
|
2021-03-02 02:29:00 +03:00
|
|
|
}
|
|
|
|
|
2021-03-18 21:34:39 +03:00
|
|
|
if fg != noColor {
|
2021-03-16 20:42:24 +03:00
|
|
|
fgc := color(fg.value())
|
2021-03-08 15:59:32 +03:00
|
|
|
te = te.Foreground(fgc)
|
|
|
|
te.Foreground(fgc)
|
|
|
|
if styleWhitespace {
|
|
|
|
teWhitespace = teWhitespace.Foreground(fgc)
|
|
|
|
}
|
|
|
|
if useSpaceStyler {
|
|
|
|
teSpace = teSpace.Foreground(fgc)
|
2021-03-04 20:54:40 +03:00
|
|
|
}
|
2021-03-02 02:29:00 +03:00
|
|
|
}
|
|
|
|
|
2021-03-18 21:34:39 +03:00
|
|
|
if bg != noColor {
|
2021-03-16 20:42:24 +03:00
|
|
|
bgc := color(bg.value())
|
2021-03-08 15:59:32 +03:00
|
|
|
te = te.Background(bgc)
|
|
|
|
if colorWhitespace {
|
|
|
|
teWhitespace = teWhitespace.Background(bgc)
|
|
|
|
}
|
|
|
|
if useSpaceStyler {
|
|
|
|
teSpace = teSpace.Background(bgc)
|
2021-03-04 20:54:40 +03:00
|
|
|
}
|
2021-03-02 02:29:00 +03:00
|
|
|
}
|
|
|
|
|
2021-03-06 00:34:46 +03:00
|
|
|
if underline {
|
2021-03-08 15:59:32 +03:00
|
|
|
te = te.Underline()
|
2021-03-06 00:34:46 +03:00
|
|
|
}
|
2021-03-08 15:59:32 +03:00
|
|
|
if strikethrough {
|
|
|
|
te = te.CrossOut()
|
2021-03-06 00:34:46 +03:00
|
|
|
}
|
|
|
|
|
2021-03-05 20:23:25 +03:00
|
|
|
if underlineWhitespace {
|
2021-03-08 15:59:32 +03:00
|
|
|
teWhitespace = teWhitespace.Underline()
|
2021-03-05 20:23:25 +03:00
|
|
|
}
|
2021-03-08 15:59:32 +03:00
|
|
|
if strikethroughWhitespace {
|
|
|
|
teWhitespace = teWhitespace.CrossOut()
|
2021-03-05 03:23:41 +03:00
|
|
|
}
|
2021-03-06 00:34:46 +03:00
|
|
|
if underlineSpaces {
|
2021-03-08 15:59:32 +03:00
|
|
|
teSpace = teSpace.Underline()
|
2021-03-02 02:29:00 +03:00
|
|
|
}
|
2021-03-06 00:34:46 +03:00
|
|
|
if strikethroughSpaces {
|
2021-03-08 15:59:32 +03:00
|
|
|
teSpace = teSpace.CrossOut()
|
2021-03-05 03:23:41 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// Strip newlines in single line mode
|
2021-03-08 15:59:32 +03:00
|
|
|
if inline {
|
2021-03-02 02:29:00 +03:00
|
|
|
str = strings.Replace(str, "\n", "", -1)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Word wrap
|
2021-03-08 15:59:32 +03:00
|
|
|
if !inline && width > 0 {
|
|
|
|
str = wordwrap.String(str, width-leftPadding-rightPadding)
|
2021-03-02 02:29:00 +03:00
|
|
|
}
|
|
|
|
|
2021-03-05 20:23:25 +03:00
|
|
|
// Render core text
|
|
|
|
{
|
|
|
|
var b strings.Builder
|
2021-03-06 00:34:46 +03:00
|
|
|
|
2021-03-05 20:23:25 +03:00
|
|
|
l := strings.Split(str, "\n")
|
|
|
|
for i := range l {
|
2021-03-06 00:34:46 +03:00
|
|
|
if useSpaceStyler {
|
|
|
|
// Look for spaces and apply a different styler
|
2021-03-18 19:39:09 +03:00
|
|
|
for _, r := range l[i] {
|
2021-03-06 00:34:46 +03:00
|
|
|
if unicode.IsSpace(r) {
|
2021-03-08 15:59:32 +03:00
|
|
|
b.WriteString(teSpace.Styled(string(r)))
|
2021-03-06 00:34:46 +03:00
|
|
|
continue
|
|
|
|
}
|
2021-03-08 15:59:32 +03:00
|
|
|
b.WriteString(te.Styled(string(r)))
|
2021-03-06 00:34:46 +03:00
|
|
|
}
|
|
|
|
} else {
|
2021-03-08 15:59:32 +03:00
|
|
|
b.WriteString(te.Styled(l[i]))
|
2021-03-06 00:34:46 +03:00
|
|
|
}
|
2021-03-05 20:23:25 +03:00
|
|
|
if i != len(l)-1 {
|
|
|
|
b.WriteRune('\n')
|
|
|
|
}
|
2021-03-04 20:54:40 +03:00
|
|
|
}
|
2021-03-05 20:23:25 +03:00
|
|
|
|
|
|
|
str = b.String()
|
2021-03-02 02:29:00 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// Left/right padding
|
2021-03-08 15:59:32 +03:00
|
|
|
if leftPadding > 0 {
|
2021-03-05 20:23:25 +03:00
|
|
|
var st *termenv.Style
|
|
|
|
if colorWhitespace || styleWhitespace {
|
2021-03-08 15:59:32 +03:00
|
|
|
st = &teWhitespace
|
2021-03-04 20:54:40 +03:00
|
|
|
}
|
2021-03-08 15:59:32 +03:00
|
|
|
str = padLeft(str, leftPadding, st)
|
2021-03-05 20:23:25 +03:00
|
|
|
}
|
2021-03-08 15:59:32 +03:00
|
|
|
if (colorWhitespace || drawClearTrailingSpaces) && rightPadding > 0 {
|
2021-03-05 20:23:25 +03:00
|
|
|
var st *termenv.Style
|
|
|
|
if colorWhitespace || styleWhitespace {
|
2021-03-08 15:59:32 +03:00
|
|
|
st = &teWhitespace
|
2021-03-04 20:54:40 +03:00
|
|
|
}
|
2021-03-08 15:59:32 +03:00
|
|
|
str = padRight(str, rightPadding, st)
|
2021-03-02 02:29:00 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// Top/bottom padding
|
2021-03-08 15:59:32 +03:00
|
|
|
if topPadding > 0 && !inline {
|
|
|
|
str = strings.Repeat("\n", topPadding) + str
|
2021-03-02 02:29:00 +03:00
|
|
|
}
|
2021-03-08 15:59:32 +03:00
|
|
|
if bottomPadding > 0 && !inline {
|
|
|
|
str += strings.Repeat("\n", bottomPadding)
|
2021-03-02 02:29:00 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// Set alignment. This will also pad short lines with spaces so that all
|
2021-03-04 20:54:40 +03:00
|
|
|
// lines are the same length, so we run it under a few different conditions
|
|
|
|
// beyond alignment.
|
|
|
|
{
|
2021-03-05 20:23:25 +03:00
|
|
|
numLines := strings.Count(str, "\n")
|
|
|
|
|
|
|
|
if numLines > 0 && (align != AlignLeft || drawClearTrailingSpaces || colorWhitespace) {
|
|
|
|
var st *termenv.Style
|
|
|
|
if colorWhitespace || styleWhitespace {
|
2021-03-08 15:59:32 +03:00
|
|
|
st = &teWhitespace
|
2021-03-02 02:29:00 +03:00
|
|
|
}
|
2021-03-05 20:23:25 +03:00
|
|
|
str = alignText(str, align, st)
|
2021-03-02 02:29:00 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-08 15:59:32 +03:00
|
|
|
// Add left and right margin
|
|
|
|
str = padLeft(str, leftMargin, nil)
|
|
|
|
str = padRight(str, rightMargin, nil)
|
2021-03-02 02:29:00 +03:00
|
|
|
|
|
|
|
// Top/bottom margin
|
2021-03-08 15:59:32 +03:00
|
|
|
if !inline {
|
2021-03-02 02:29:00 +03:00
|
|
|
var maybeSpaces string
|
|
|
|
|
2021-03-05 03:23:41 +03:00
|
|
|
if drawClearTrailingSpaces {
|
2021-03-02 02:29:00 +03:00
|
|
|
_, width := getLines(str)
|
|
|
|
maybeSpaces = strings.Repeat(" ", width)
|
|
|
|
}
|
|
|
|
|
2021-03-08 15:59:32 +03:00
|
|
|
if topMargin > 0 {
|
|
|
|
str = strings.Repeat(maybeSpaces+"\n", topMargin) + str
|
2021-03-02 02:29:00 +03:00
|
|
|
}
|
2021-03-08 15:59:32 +03:00
|
|
|
if bottomMargin > 0 {
|
|
|
|
str += strings.Repeat("\n"+maybeSpaces, bottomMargin)
|
2021-03-02 02:29:00 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-10 18:21:21 +03:00
|
|
|
// Truncate according to MaxWidth
|
2021-03-08 15:59:32 +03:00
|
|
|
if maxWidth > 0 {
|
2021-03-02 02:29:00 +03:00
|
|
|
lines := strings.Split(str, "\n")
|
|
|
|
|
|
|
|
for i := range lines {
|
2021-03-08 15:59:32 +03:00
|
|
|
lines[i] = truncate.String(lines[i], uint(maxWidth))
|
2021-03-02 02:29:00 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
str = strings.Join(lines, "\n")
|
|
|
|
}
|
|
|
|
|
|
|
|
return str
|
|
|
|
}
|
|
|
|
|
|
|
|
// Apply left padding.
|
2021-03-05 20:23:25 +03:00
|
|
|
func padLeft(str string, n int, style *termenv.Style) string {
|
|
|
|
if n == 0 {
|
2021-03-02 02:29:00 +03:00
|
|
|
return str
|
|
|
|
}
|
2021-03-05 20:23:25 +03:00
|
|
|
|
|
|
|
sp := strings.Repeat(" ", n)
|
|
|
|
if style != nil {
|
|
|
|
sp = style.Styled(sp)
|
|
|
|
}
|
|
|
|
|
|
|
|
b := strings.Builder{}
|
|
|
|
l := strings.Split(str, "\n")
|
|
|
|
|
|
|
|
for i := range l {
|
|
|
|
b.WriteString(sp)
|
|
|
|
b.WriteString(l[i])
|
|
|
|
if i != len(l)-1 {
|
|
|
|
b.WriteRune('\n')
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return b.String()
|
2021-03-02 02:29:00 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// Apply right right padding.
|
2021-03-05 20:23:25 +03:00
|
|
|
func padRight(str string, n int, style *termenv.Style) string {
|
2021-03-02 02:29:00 +03:00
|
|
|
if n == 0 || str == "" {
|
|
|
|
return str
|
|
|
|
}
|
|
|
|
|
2021-03-05 20:23:25 +03:00
|
|
|
sp := strings.Repeat(" ", n)
|
|
|
|
if style != nil {
|
|
|
|
sp = style.Styled(sp)
|
2021-03-02 02:29:00 +03:00
|
|
|
}
|
|
|
|
|
2021-03-05 20:23:25 +03:00
|
|
|
b := strings.Builder{}
|
|
|
|
l := strings.Split(str, "\n")
|
|
|
|
|
|
|
|
for i := range l {
|
|
|
|
b.WriteString(l[i])
|
|
|
|
b.WriteString(sp)
|
|
|
|
if i != len(l)-1 {
|
|
|
|
b.WriteRune('\n')
|
|
|
|
}
|
2021-03-02 02:29:00 +03:00
|
|
|
}
|
|
|
|
|
2021-03-05 20:23:25 +03:00
|
|
|
return b.String()
|
2021-03-02 02:29:00 +03:00
|
|
|
}
|