1package lipgloss 2 3import ( 4 "strings" 5 "unicode" 6 7 "github.com/muesli/reflow/truncate" 8 "github.com/muesli/reflow/wordwrap" 9 "github.com/muesli/reflow/wrap" 10 "github.com/muesli/termenv" 11) 12 13// Property for a key. 14type propKey int 15 16// Available properties. 17const ( 18 boldKey propKey = iota 19 italicKey 20 underlineKey 21 strikethroughKey 22 reverseKey 23 blinkKey 24 faintKey 25 foregroundKey 26 backgroundKey 27 widthKey 28 heightKey 29 alignKey 30 31 // Padding. 32 paddingTopKey 33 paddingRightKey 34 paddingBottomKey 35 paddingLeftKey 36 37 colorWhitespaceKey 38 39 // Margins. 40 marginTopKey 41 marginRightKey 42 marginBottomKey 43 marginLeftKey 44 marginBackgroundKey 45 46 // Border runes. 47 borderStyleKey 48 49 // Border edges. 50 borderTopKey 51 borderRightKey 52 borderBottomKey 53 borderLeftKey 54 55 // Border foreground colors. 56 borderTopForegroundKey 57 borderRightForegroundKey 58 borderBottomForegroundKey 59 borderLeftForegroundKey 60 61 // Border background colors. 62 borderTopBackgroundKey 63 borderRightBackgroundKey 64 borderBottomBackgroundKey 65 borderLeftBackgroundKey 66 67 inlineKey 68 maxWidthKey 69 maxHeightKey 70 underlineSpacesKey 71 strikethroughSpacesKey 72) 73 74// A set of properties. 75type rules map[propKey]interface{} 76 77// NewStyle returns a new, empty Style. While it's syntactic sugar for the 78// Style{} primitive, it's recommended to use this function for creating styles 79// incase the underlying implementation changes. 80func NewStyle() Style { 81 return Style{} 82} 83 84// Style contains a set of rules that comprise a style as a whole. 85type Style struct { 86 rules map[propKey]interface{} 87 value string 88} 89 90// SetString sets the underlying string value for this style. To render once 91// the underlying string is set, use the Style.String. This method is 92// a convenience for cases when having a stringer implementation is handy, such 93// as when using fmt.Sprintf. You can also simply define a style and render out 94// strings directly with Style.Render. 95func (s Style) SetString(str string) Style { 96 s.value = str 97 return s 98} 99 100// String implements stringer for a Style, returning the rendered result based 101// on the rules in this style. An underlying string value must be set with 102// Style.SetString prior to using this method. 103func (s Style) String() string { 104 return s.Render(s.value) 105} 106 107// Copy returns a copy of this style, including any underlying string values. 108func (s Style) Copy() Style { 109 o := NewStyle() 110 o.init() 111 for k, v := range s.rules { 112 o.rules[k] = v 113 } 114 o.value = s.value 115 return o 116} 117 118// Inherit takes values from the style in the argument applies them to this 119// style, overwriting existing definitions. Only values explicitly set on the 120// style in argument will be applied. 121// 122// Margins, padding, and underlying string values are not inherited. 123func (s Style) Inherit(i Style) Style { 124 s.init() 125 126 for k, v := range i.rules { 127 switch k { 128 case marginTopKey, marginRightKey, marginBottomKey, marginLeftKey: 129 // Margins are not inherited 130 continue 131 case paddingTopKey, paddingRightKey, paddingBottomKey, paddingLeftKey: 132 // Padding is not inherited 133 continue 134 case backgroundKey: 135 s.rules[k] = v 136 137 // The margins also inherit the background color 138 if !s.isSet(marginBackgroundKey) && !i.isSet(marginBackgroundKey) { 139 s.rules[marginBackgroundKey] = v 140 } 141 } 142 143 if _, exists := s.rules[k]; exists { 144 continue 145 } 146 s.rules[k] = v 147 } 148 return s 149} 150 151// Render applies the defined style formatting to a given string. 152func (s Style) Render(str string) string { 153 var ( 154 te termenv.Style 155 teSpace termenv.Style 156 teWhitespace termenv.Style 157 158 bold = s.getAsBool(boldKey, false) 159 italic = s.getAsBool(italicKey, false) 160 underline = s.getAsBool(underlineKey, false) 161 strikethrough = s.getAsBool(strikethroughKey, false) 162 reverse = s.getAsBool(reverseKey, false) 163 blink = s.getAsBool(blinkKey, false) 164 faint = s.getAsBool(faintKey, false) 165 166 fg = s.getAsColor(foregroundKey) 167 bg = s.getAsColor(backgroundKey) 168 169 width = s.getAsInt(widthKey) 170 height = s.getAsInt(heightKey) 171 align = s.getAsPosition(alignKey) 172 173 topPadding = s.getAsInt(paddingTopKey) 174 rightPadding = s.getAsInt(paddingRightKey) 175 bottomPadding = s.getAsInt(paddingBottomKey) 176 leftPadding = s.getAsInt(paddingLeftKey) 177 178 colorWhitespace = s.getAsBool(colorWhitespaceKey, true) 179 inline = s.getAsBool(inlineKey, false) 180 maxWidth = s.getAsInt(maxWidthKey) 181 maxHeight = s.getAsInt(maxHeightKey) 182 183 underlineSpaces = underline && s.getAsBool(underlineSpacesKey, true) 184 strikethroughSpaces = strikethrough && s.getAsBool(strikethroughSpacesKey, true) 185 186 // Do we need to style whitespace (padding and space outside 187 // paragraphs) separately? 188 styleWhitespace = reverse 189 190 // Do we need to style spaces separately? 191 useSpaceStyler = underlineSpaces || strikethroughSpaces 192 ) 193 194 if len(s.rules) == 0 { 195 return str 196 } 197 198 // Enable support for ANSI on the legacy Windows cmd.exe console. This is a 199 // no-op on non-Windows systems and on Windows runs only once. 200 enableLegacyWindowsANSI() 201 202 if bold { 203 te = te.Bold() 204 } 205 if italic { 206 te = te.Italic() 207 } 208 if underline { 209 te = te.Underline() 210 } 211 if reverse { 212 if reverse { 213 teWhitespace = teWhitespace.Reverse() 214 } 215 te = te.Reverse() 216 } 217 if blink { 218 te = te.Blink() 219 } 220 if faint { 221 te = te.Faint() 222 } 223 224 if fg != noColor { 225 fgc := fg.color() 226 te = te.Foreground(fgc) 227 if styleWhitespace { 228 teWhitespace = teWhitespace.Foreground(fgc) 229 } 230 if useSpaceStyler { 231 teSpace = teSpace.Foreground(fgc) 232 } 233 } 234 235 if bg != noColor { 236 bgc := bg.color() 237 te = te.Background(bgc) 238 if colorWhitespace { 239 teWhitespace = teWhitespace.Background(bgc) 240 } 241 if useSpaceStyler { 242 teSpace = teSpace.Background(bgc) 243 } 244 } 245 246 if underline { 247 te = te.Underline() 248 } 249 if strikethrough { 250 te = te.CrossOut() 251 } 252 253 if underlineSpaces { 254 teSpace = teSpace.Underline() 255 } 256 if strikethroughSpaces { 257 teSpace = teSpace.CrossOut() 258 } 259 260 // Strip newlines in single line mode 261 if inline { 262 str = strings.Replace(str, "\n", "", -1) 263 } 264 265 // Word wrap 266 if !inline && width > 0 { 267 wrapAt := width - leftPadding - rightPadding 268 str = wordwrap.String(str, wrapAt) 269 str = wrap.String(str, wrapAt) // force-wrap long strings 270 } 271 272 // Render core text 273 { 274 var b strings.Builder 275 276 l := strings.Split(str, "\n") 277 for i := range l { 278 if useSpaceStyler { 279 // Look for spaces and apply a different styler 280 for _, r := range l[i] { 281 if unicode.IsSpace(r) { 282 b.WriteString(teSpace.Styled(string(r))) 283 continue 284 } 285 b.WriteString(te.Styled(string(r))) 286 } 287 } else { 288 b.WriteString(te.Styled(l[i])) 289 } 290 if i != len(l)-1 { 291 b.WriteRune('\n') 292 } 293 } 294 295 str = b.String() 296 } 297 298 // Padding 299 if !inline { 300 if leftPadding > 0 { 301 var st *termenv.Style 302 if colorWhitespace || styleWhitespace { 303 st = &teWhitespace 304 } 305 str = padLeft(str, leftPadding, st) 306 } 307 308 if rightPadding > 0 { 309 var st *termenv.Style 310 if colorWhitespace || styleWhitespace { 311 st = &teWhitespace 312 } 313 str = padRight(str, rightPadding, st) 314 } 315 316 if topPadding > 0 { 317 str = strings.Repeat("\n", topPadding) + str 318 } 319 320 if bottomPadding > 0 { 321 str += strings.Repeat("\n", bottomPadding) 322 } 323 } 324 325 // Height 326 if height > 0 { 327 h := strings.Count(str, "\n") + 1 328 if height > h { 329 str += strings.Repeat("\n", height-h) 330 } 331 } 332 333 // Set alignment. This will also pad short lines with spaces so that all 334 // lines are the same length, so we run it under a few different conditions 335 // beyond alignment. 336 { 337 numLines := strings.Count(str, "\n") 338 339 if !(numLines == 0 && width == 0) { 340 var st *termenv.Style 341 if colorWhitespace || styleWhitespace { 342 st = &teWhitespace 343 } 344 str = alignText(str, align, width, st) 345 } 346 } 347 348 if !inline { 349 str = s.applyBorder(str) 350 str = s.applyMargins(str, inline) 351 } 352 353 // Truncate according to MaxWidth 354 if maxWidth > 0 { 355 lines := strings.Split(str, "\n") 356 357 for i := range lines { 358 lines[i] = truncate.String(lines[i], uint(maxWidth)) 359 } 360 361 str = strings.Join(lines, "\n") 362 } 363 364 // Truncate according to MaxHeight 365 if maxHeight > 0 { 366 lines := strings.Split(str, "\n") 367 str = strings.Join(lines[:min(maxHeight, len(lines))], "\n") 368 } 369 370 return str 371} 372 373func (s Style) applyMargins(str string, inline bool) string { 374 var ( 375 topMargin = s.getAsInt(marginTopKey) 376 rightMargin = s.getAsInt(marginRightKey) 377 bottomMargin = s.getAsInt(marginBottomKey) 378 leftMargin = s.getAsInt(marginLeftKey) 379 380 styler termenv.Style 381 ) 382 383 bgc := s.getAsColor(marginBackgroundKey) 384 if bgc != noColor { 385 styler = styler.Background(bgc.color()) 386 } 387 388 // Add left and right margin 389 str = padLeft(str, leftMargin, &styler) 390 str = padRight(str, rightMargin, &styler) 391 392 // Top/bottom margin 393 if !inline { 394 _, width := getLines(str) 395 spaces := strings.Repeat(" ", width) 396 397 if topMargin > 0 { 398 str = styler.Styled(strings.Repeat(spaces+"\n", topMargin)) + str 399 } 400 if bottomMargin > 0 { 401 str += styler.Styled(strings.Repeat("\n"+spaces, bottomMargin)) 402 } 403 } 404 405 return str 406} 407 408// Apply left padding. 409func padLeft(str string, n int, style *termenv.Style) string { 410 if n == 0 { 411 return str 412 } 413 414 sp := strings.Repeat(" ", n) 415 if style != nil { 416 sp = style.Styled(sp) 417 } 418 419 b := strings.Builder{} 420 l := strings.Split(str, "\n") 421 422 for i := range l { 423 b.WriteString(sp) 424 b.WriteString(l[i]) 425 if i != len(l)-1 { 426 b.WriteRune('\n') 427 } 428 } 429 430 return b.String() 431} 432 433// Apply right right padding. 434func padRight(str string, n int, style *termenv.Style) string { 435 if n == 0 || str == "" { 436 return str 437 } 438 439 sp := strings.Repeat(" ", n) 440 if style != nil { 441 sp = style.Styled(sp) 442 } 443 444 b := strings.Builder{} 445 l := strings.Split(str, "\n") 446 447 for i := range l { 448 b.WriteString(l[i]) 449 b.WriteString(sp) 450 if i != len(l)-1 { 451 b.WriteRune('\n') 452 } 453 } 454 455 return b.String() 456} 457 458func max(a, b int) int { 459 if a > b { 460 return a 461 } 462 return b 463} 464 465func min(a, b int) int { 466 if a < b { 467 return a 468 } 469 return b 470} 471