package lipgloss import ( "strings" "unicode" "github.com/muesli/reflow/truncate" "github.com/muesli/reflow/wordwrap" "github.com/muesli/reflow/wrap" "github.com/muesli/termenv" ) const tabWidthDefault = 4 // Property for a key. type propKey int // Available properties. const ( boldKey propKey = iota italicKey underlineKey strikethroughKey reverseKey blinkKey faintKey foregroundKey backgroundKey widthKey heightKey alignHorizontalKey alignVerticalKey // Padding. paddingTopKey paddingRightKey paddingBottomKey paddingLeftKey colorWhitespaceKey // Margins. marginTopKey marginRightKey marginBottomKey marginLeftKey marginBackgroundKey // Border runes. borderStyleKey // Border edges. borderTopKey borderRightKey borderBottomKey borderLeftKey // Border foreground colors. borderTopForegroundKey borderRightForegroundKey borderBottomForegroundKey borderLeftForegroundKey // Border background colors. borderTopBackgroundKey borderRightBackgroundKey borderBottomBackgroundKey borderLeftBackgroundKey inlineKey maxWidthKey maxHeightKey tabWidthKey underlineSpacesKey strikethroughSpacesKey transformKey ) // A set of properties. type rules map[propKey]interface{} // NewStyle returns a new, empty Style. While it's syntactic sugar for the // Style{} primitive, it's recommended to use this function for creating styles // in case the underlying implementation changes. It takes an optional string // value to be set as the underlying string value for this style. func NewStyle() Style { return renderer.NewStyle() } // NewStyle returns a new, empty Style. While it's syntactic sugar for the // Style{} primitive, it's recommended to use this function for creating styles // in case the underlying implementation changes. It takes an optional string // value to be set as the underlying string value for this style. func (r *Renderer) NewStyle() Style { s := Style{r: r} return s } // Style contains a set of rules that comprise a style as a whole. type Style struct { r *Renderer rules map[propKey]interface{} value string } // joinString joins a list of strings into a single string separated with a // space. func joinString(strs ...string) string { return strings.Join(strs, " ") } // 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(strs ...string) Style { s.value = joinString(strs...) return s } // Value returns the raw, unformatted, underlying string value for this style. func (s Style) Value() string { return s.value } // 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() } // Copy returns a copy of this style, including any underlying string values. func (s Style) Copy() Style { o := NewStyle() o.init() for k, v := range s.rules { o.rules[k] = v } o.r = s.r o.value = s.value return o } // Inherit overlays the style in the argument onto this style by copying each explicitly // set value from the argument style onto this style if it is not already explicitly set. // Existing set values are kept intact and not overwritten. // // Margins, padding, and underlying string values are not inherited. func (s Style) Inherit(i Style) Style { s.init() for k, v := range i.rules { switch k { //nolint:exhaustive case marginTopKey, marginRightKey, marginBottomKey, marginLeftKey: // Margins are not inherited continue case paddingTopKey, paddingRightKey, paddingBottomKey, paddingLeftKey: // Padding is not inherited continue case backgroundKey: // The margins also inherit the background color if !s.isSet(marginBackgroundKey) && !i.isSet(marginBackgroundKey) { s.rules[marginBackgroundKey] = v } } if _, exists := s.rules[k]; exists { continue } s.rules[k] = v } return s } // Render applies the defined style formatting to a given string. func (s Style) Render(strs ...string) string { if s.r == nil { s.r = renderer } if s.value != "" { strs = append([]string{s.value}, strs...) } var ( str = joinString(strs...) p = s.r.ColorProfile() te = p.String() teSpace = p.String() teWhitespace = p.String() 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) height = s.getAsInt(heightKey) horizontalAlign = s.getAsPosition(alignHorizontalKey) verticalAlign = s.getAsPosition(alignVerticalKey) topPadding = s.getAsInt(paddingTopKey) rightPadding = s.getAsInt(paddingRightKey) bottomPadding = s.getAsInt(paddingBottomKey) leftPadding = s.getAsInt(paddingLeftKey) colorWhitespace = s.getAsBool(colorWhitespaceKey, true) inline = s.getAsBool(inlineKey, false) maxWidth = s.getAsInt(maxWidthKey) maxHeight = s.getAsInt(maxHeightKey) underlineSpaces = underline && s.getAsBool(underlineSpacesKey, true) strikethroughSpaces = strikethrough && s.getAsBool(strikethroughSpacesKey, true) // Do we need to style whitespace (padding and space outside // paragraphs) separately? styleWhitespace = reverse // Do we need to style spaces separately? useSpaceStyler = underlineSpaces || strikethroughSpaces transform = s.getAsTransform(transformKey) ) if len(s.rules) == 0 { return s.maybeConvertTabs(str) } // Enable support for ANSI on the legacy Windows cmd.exe console. This is a // no-op on non-Windows systems and on Windows runs only once. enableLegacyWindowsANSI() if bold { te = te.Bold() } if italic { te = te.Italic() } if underline { te = te.Underline() } if reverse { if reverse { teWhitespace = teWhitespace.Reverse() } te = te.Reverse() } if blink { te = te.Blink() } if faint { te = te.Faint() } if fg != noColor { te = te.Foreground(fg.color(s.r)) if styleWhitespace { teWhitespace = teWhitespace.Foreground(fg.color(s.r)) } if useSpaceStyler { teSpace = teSpace.Foreground(fg.color(s.r)) } } if bg != noColor { te = te.Background(bg.color(s.r)) if colorWhitespace { teWhitespace = teWhitespace.Background(bg.color(s.r)) } if useSpaceStyler { teSpace = teSpace.Background(bg.color(s.r)) } } if underline { te = te.Underline() } if strikethrough { te = te.CrossOut() } if underlineSpaces { teSpace = teSpace.Underline() } if strikethroughSpaces { teSpace = teSpace.CrossOut() } // Potentially convert tabs to spaces str = s.maybeConvertTabs(str) // Strip newlines in single line mode if inline { str = strings.ReplaceAll(str, "\n", "") } // Word wrap if !inline && width > 0 { wrapAt := width - leftPadding - rightPadding str = wordwrap.String(str, wrapAt) str = wrap.String(str, wrapAt) // force-wrap long strings } // Render core text { var b strings.Builder l := strings.Split(str, "\n") for i := range l { if useSpaceStyler { // Look for spaces and apply a different styler for _, r := range l[i] { if unicode.IsSpace(r) { b.WriteString(teSpace.Styled(string(r))) continue } b.WriteString(te.Styled(string(r))) } } else { b.WriteString(te.Styled(l[i])) } if i != len(l)-1 { b.WriteRune('\n') } } str = b.String() } // Padding if !inline { if leftPadding > 0 { var st *termenv.Style if colorWhitespace || styleWhitespace { st = &teWhitespace } str = padLeft(str, leftPadding, st) } if rightPadding > 0 { var st *termenv.Style if colorWhitespace || styleWhitespace { st = &teWhitespace } str = padRight(str, rightPadding, st) } if topPadding > 0 { str = strings.Repeat("\n", topPadding) + str } if bottomPadding > 0 { str += strings.Repeat("\n", bottomPadding) } } // Height if height > 0 { str = alignTextVertical(str, verticalAlign, height, nil) } // Set alignment. This will also pad short lines with spaces so that all // lines are the same length, so we run it under a few different conditions // beyond alignment. { numLines := strings.Count(str, "\n") if !(numLines == 0 && width == 0) { var st *termenv.Style if colorWhitespace || styleWhitespace { st = &teWhitespace } str = alignTextHorizontal(str, horizontalAlign, width, st) } } if !inline { str = s.applyBorder(str) str = s.applyMargins(str, inline) } // Truncate according to MaxWidth if maxWidth > 0 { lines := strings.Split(str, "\n") for i := range lines { lines[i] = truncate.String(lines[i], uint(maxWidth)) } str = strings.Join(lines, "\n") } // Truncate according to MaxHeight if maxHeight > 0 { lines := strings.Split(str, "\n") str = strings.Join(lines[:min(maxHeight, len(lines))], "\n") } if transform != nil { return transform(str) } return str } func (s Style) maybeConvertTabs(str string) string { tw := tabWidthDefault if s.isSet(tabWidthKey) { tw = s.getAsInt(tabWidthKey) } switch tw { case -1: return str case 0: return strings.ReplaceAll(str, "\t", "") default: return strings.ReplaceAll(str, "\t", strings.Repeat(" ", tw)) } } func (s Style) applyMargins(str string, inline bool) string { var ( topMargin = s.getAsInt(marginTopKey) rightMargin = s.getAsInt(marginRightKey) bottomMargin = s.getAsInt(marginBottomKey) leftMargin = s.getAsInt(marginLeftKey) styler termenv.Style ) bgc := s.getAsColor(marginBackgroundKey) if bgc != noColor { styler = styler.Background(bgc.color(s.r)) } // Add left and right margin str = padLeft(str, leftMargin, &styler) str = padRight(str, rightMargin, &styler) // Top/bottom margin if !inline { _, width := getLines(str) spaces := strings.Repeat(" ", width) if topMargin > 0 { str = styler.Styled(strings.Repeat(spaces+"\n", topMargin)) + str } if bottomMargin > 0 { str += styler.Styled(strings.Repeat("\n"+spaces, bottomMargin)) } } return str } // Apply left padding. func padLeft(str string, n int, style *termenv.Style) string { return pad(str, -n, style) } // Apply right padding. func padRight(str string, n int, style *termenv.Style) string { return pad(str, n, style) } // pad adds padding to either the left or right side of a string. // Positive values add to the right side while negative values // add to the left side. func pad(str string, n int, style *termenv.Style) string { if n == 0 { return str } sp := strings.Repeat(" ", abs(n)) if style != nil { sp = style.Styled(sp) } b := strings.Builder{} l := strings.Split(str, "\n") for i := range l { switch { // pad right case n > 0: b.WriteString(l[i]) b.WriteString(sp) // pad left default: b.WriteString(sp) b.WriteString(l[i]) } if i != len(l)-1 { b.WriteRune('\n') } } return b.String() } func max(a, b int) int { if a > b { return a } return b } func min(a, b int) int { if a < b { return a } return b } func abs(a int) int { if a < 0 { return -a } return a }