main
1package lipgloss
2
3import (
4 "strings"
5 "unicode"
6
7 "github.com/charmbracelet/x/ansi"
8 "github.com/charmbracelet/x/cellbuf"
9 "github.com/muesli/termenv"
10)
11
12const tabWidthDefault = 4
13
14// Property for a key.
15type propKey int64
16
17// Available properties.
18const (
19 // Boolean props come first.
20 boldKey propKey = 1 << iota
21 italicKey
22 underlineKey
23 strikethroughKey
24 reverseKey
25 blinkKey
26 faintKey
27 underlineSpacesKey
28 strikethroughSpacesKey
29 colorWhitespaceKey
30
31 // Non-boolean props.
32 foregroundKey
33 backgroundKey
34 widthKey
35 heightKey
36 alignHorizontalKey
37 alignVerticalKey
38
39 // Padding.
40 paddingTopKey
41 paddingRightKey
42 paddingBottomKey
43 paddingLeftKey
44
45 // Margins.
46 marginTopKey
47 marginRightKey
48 marginBottomKey
49 marginLeftKey
50 marginBackgroundKey
51
52 // Border runes.
53 borderStyleKey
54
55 // Border edges.
56 borderTopKey
57 borderRightKey
58 borderBottomKey
59 borderLeftKey
60
61 // Border foreground colors.
62 borderTopForegroundKey
63 borderRightForegroundKey
64 borderBottomForegroundKey
65 borderLeftForegroundKey
66
67 // Border background colors.
68 borderTopBackgroundKey
69 borderRightBackgroundKey
70 borderBottomBackgroundKey
71 borderLeftBackgroundKey
72
73 inlineKey
74 maxWidthKey
75 maxHeightKey
76 tabWidthKey
77
78 transformKey
79)
80
81// props is a set of properties.
82type props int64
83
84// set sets a property.
85func (p props) set(k propKey) props {
86 return p | props(k)
87}
88
89// unset unsets a property.
90func (p props) unset(k propKey) props {
91 return p &^ props(k)
92}
93
94// has checks if a property is set.
95func (p props) has(k propKey) bool {
96 return p&props(k) != 0
97}
98
99// NewStyle returns a new, empty Style. While it's syntactic sugar for the
100// Style{} primitive, it's recommended to use this function for creating styles
101// in case the underlying implementation changes. It takes an optional string
102// value to be set as the underlying string value for this style.
103func NewStyle() Style {
104 return renderer.NewStyle()
105}
106
107// NewStyle returns a new, empty Style. While it's syntactic sugar for the
108// Style{} primitive, it's recommended to use this function for creating styles
109// in case the underlying implementation changes. It takes an optional string
110// value to be set as the underlying string value for this style.
111func (r *Renderer) NewStyle() Style {
112 s := Style{r: r}
113 return s
114}
115
116// Style contains a set of rules that comprise a style as a whole.
117type Style struct {
118 r *Renderer
119 props props
120 value string
121
122 // we store bool props values here
123 attrs int
124
125 // props that have values
126 fgColor TerminalColor
127 bgColor TerminalColor
128
129 width int
130 height int
131
132 alignHorizontal Position
133 alignVertical Position
134
135 paddingTop int
136 paddingRight int
137 paddingBottom int
138 paddingLeft int
139
140 marginTop int
141 marginRight int
142 marginBottom int
143 marginLeft int
144 marginBgColor TerminalColor
145
146 borderStyle Border
147 borderTopFgColor TerminalColor
148 borderRightFgColor TerminalColor
149 borderBottomFgColor TerminalColor
150 borderLeftFgColor TerminalColor
151 borderTopBgColor TerminalColor
152 borderRightBgColor TerminalColor
153 borderBottomBgColor TerminalColor
154 borderLeftBgColor TerminalColor
155
156 maxWidth int
157 maxHeight int
158 tabWidth int
159
160 transform func(string) string
161}
162
163// joinString joins a list of strings into a single string separated with a
164// space.
165func joinString(strs ...string) string {
166 return strings.Join(strs, " ")
167}
168
169// SetString sets the underlying string value for this style. To render once
170// the underlying string is set, use the Style.String. This method is
171// a convenience for cases when having a stringer implementation is handy, such
172// as when using fmt.Sprintf. You can also simply define a style and render out
173// strings directly with Style.Render.
174func (s Style) SetString(strs ...string) Style {
175 s.value = joinString(strs...)
176 return s
177}
178
179// Value returns the raw, unformatted, underlying string value for this style.
180func (s Style) Value() string {
181 return s.value
182}
183
184// String implements stringer for a Style, returning the rendered result based
185// on the rules in this style. An underlying string value must be set with
186// Style.SetString prior to using this method.
187func (s Style) String() string {
188 return s.Render()
189}
190
191// Copy returns a copy of this style, including any underlying string values.
192//
193// Deprecated: to copy just use assignment (i.e. a := b). All methods also
194// return a new style.
195func (s Style) Copy() Style {
196 return s
197}
198
199// Inherit overlays the style in the argument onto this style by copying each explicitly
200// set value from the argument style onto this style if it is not already explicitly set.
201// Existing set values are kept intact and not overwritten.
202//
203// Margins, padding, and underlying string values are not inherited.
204func (s Style) Inherit(i Style) Style {
205 for k := boldKey; k <= transformKey; k <<= 1 {
206 if !i.isSet(k) {
207 continue
208 }
209
210 switch k { //nolint:exhaustive
211 case marginTopKey, marginRightKey, marginBottomKey, marginLeftKey:
212 // Margins are not inherited
213 continue
214 case paddingTopKey, paddingRightKey, paddingBottomKey, paddingLeftKey:
215 // Padding is not inherited
216 continue
217 case backgroundKey:
218 // The margins also inherit the background color
219 if !s.isSet(marginBackgroundKey) && !i.isSet(marginBackgroundKey) {
220 s.set(marginBackgroundKey, i.bgColor)
221 }
222 }
223
224 if s.isSet(k) {
225 continue
226 }
227
228 s.setFrom(k, i)
229 }
230 return s
231}
232
233// Render applies the defined style formatting to a given string.
234func (s Style) Render(strs ...string) string {
235 if s.r == nil {
236 s.r = renderer
237 }
238 if s.value != "" {
239 strs = append([]string{s.value}, strs...)
240 }
241
242 var (
243 str = joinString(strs...)
244
245 p = s.r.ColorProfile()
246 te = p.String()
247 teSpace = p.String()
248 teWhitespace = p.String()
249
250 bold = s.getAsBool(boldKey, false)
251 italic = s.getAsBool(italicKey, false)
252 underline = s.getAsBool(underlineKey, false)
253 strikethrough = s.getAsBool(strikethroughKey, false)
254 reverse = s.getAsBool(reverseKey, false)
255 blink = s.getAsBool(blinkKey, false)
256 faint = s.getAsBool(faintKey, false)
257
258 fg = s.getAsColor(foregroundKey)
259 bg = s.getAsColor(backgroundKey)
260
261 width = s.getAsInt(widthKey)
262 height = s.getAsInt(heightKey)
263 horizontalAlign = s.getAsPosition(alignHorizontalKey)
264 verticalAlign = s.getAsPosition(alignVerticalKey)
265
266 topPadding = s.getAsInt(paddingTopKey)
267 rightPadding = s.getAsInt(paddingRightKey)
268 bottomPadding = s.getAsInt(paddingBottomKey)
269 leftPadding = s.getAsInt(paddingLeftKey)
270
271 colorWhitespace = s.getAsBool(colorWhitespaceKey, true)
272 inline = s.getAsBool(inlineKey, false)
273 maxWidth = s.getAsInt(maxWidthKey)
274 maxHeight = s.getAsInt(maxHeightKey)
275
276 underlineSpaces = s.getAsBool(underlineSpacesKey, false) || (underline && s.getAsBool(underlineSpacesKey, true))
277 strikethroughSpaces = s.getAsBool(strikethroughSpacesKey, false) || (strikethrough && s.getAsBool(strikethroughSpacesKey, true))
278
279 // Do we need to style whitespace (padding and space outside
280 // paragraphs) separately?
281 styleWhitespace = reverse
282
283 // Do we need to style spaces separately?
284 useSpaceStyler = (underline && !underlineSpaces) || (strikethrough && !strikethroughSpaces) || underlineSpaces || strikethroughSpaces
285
286 transform = s.getAsTransform(transformKey)
287 )
288
289 if transform != nil {
290 str = transform(str)
291 }
292
293 if s.props == 0 {
294 return s.maybeConvertTabs(str)
295 }
296
297 // Enable support for ANSI on the legacy Windows cmd.exe console. This is a
298 // no-op on non-Windows systems and on Windows runs only once.
299 enableLegacyWindowsANSI()
300
301 if bold {
302 te = te.Bold()
303 }
304 if italic {
305 te = te.Italic()
306 }
307 if underline {
308 te = te.Underline()
309 }
310 if reverse {
311 teWhitespace = teWhitespace.Reverse()
312 te = te.Reverse()
313 }
314 if blink {
315 te = te.Blink()
316 }
317 if faint {
318 te = te.Faint()
319 }
320
321 if fg != noColor {
322 te = te.Foreground(fg.color(s.r))
323 if styleWhitespace {
324 teWhitespace = teWhitespace.Foreground(fg.color(s.r))
325 }
326 if useSpaceStyler {
327 teSpace = teSpace.Foreground(fg.color(s.r))
328 }
329 }
330
331 if bg != noColor {
332 te = te.Background(bg.color(s.r))
333 if colorWhitespace {
334 teWhitespace = teWhitespace.Background(bg.color(s.r))
335 }
336 if useSpaceStyler {
337 teSpace = teSpace.Background(bg.color(s.r))
338 }
339 }
340
341 if underline {
342 te = te.Underline()
343 }
344 if strikethrough {
345 te = te.CrossOut()
346 }
347
348 if underlineSpaces {
349 teSpace = teSpace.Underline()
350 }
351 if strikethroughSpaces {
352 teSpace = teSpace.CrossOut()
353 }
354
355 // Potentially convert tabs to spaces
356 str = s.maybeConvertTabs(str)
357 // carriage returns can cause strange behaviour when rendering.
358 str = strings.ReplaceAll(str, "\r\n", "\n")
359
360 // Strip newlines in single line mode
361 if inline {
362 str = strings.ReplaceAll(str, "\n", "")
363 }
364
365 // Word wrap
366 if !inline && width > 0 {
367 wrapAt := width - leftPadding - rightPadding
368 str = cellbuf.Wrap(str, wrapAt, "")
369 }
370
371 // Render core text
372 {
373 var b strings.Builder
374
375 l := strings.Split(str, "\n")
376 for i := range l {
377 if useSpaceStyler {
378 // Look for spaces and apply a different styler
379 for _, r := range l[i] {
380 if unicode.IsSpace(r) {
381 b.WriteString(teSpace.Styled(string(r)))
382 continue
383 }
384 b.WriteString(te.Styled(string(r)))
385 }
386 } else {
387 b.WriteString(te.Styled(l[i]))
388 }
389 if i != len(l)-1 {
390 b.WriteRune('\n')
391 }
392 }
393
394 str = b.String()
395 }
396
397 // Padding
398 if !inline { //nolint:nestif
399 if leftPadding > 0 {
400 var st *termenv.Style
401 if colorWhitespace || styleWhitespace {
402 st = &teWhitespace
403 }
404 str = padLeft(str, leftPadding, st)
405 }
406
407 if rightPadding > 0 {
408 var st *termenv.Style
409 if colorWhitespace || styleWhitespace {
410 st = &teWhitespace
411 }
412 str = padRight(str, rightPadding, st)
413 }
414
415 if topPadding > 0 {
416 str = strings.Repeat("\n", topPadding) + str
417 }
418
419 if bottomPadding > 0 {
420 str += strings.Repeat("\n", bottomPadding)
421 }
422 }
423
424 // Height
425 if height > 0 {
426 str = alignTextVertical(str, verticalAlign, height, nil)
427 }
428
429 // Set alignment. This will also pad short lines with spaces so that all
430 // lines are the same length, so we run it under a few different conditions
431 // beyond alignment.
432 {
433 numLines := strings.Count(str, "\n")
434
435 if numLines != 0 || width != 0 {
436 var st *termenv.Style
437 if colorWhitespace || styleWhitespace {
438 st = &teWhitespace
439 }
440 str = alignTextHorizontal(str, horizontalAlign, width, st)
441 }
442 }
443
444 if !inline {
445 str = s.applyBorder(str)
446 str = s.applyMargins(str, inline)
447 }
448
449 // Truncate according to MaxWidth
450 if maxWidth > 0 {
451 lines := strings.Split(str, "\n")
452
453 for i := range lines {
454 lines[i] = ansi.Truncate(lines[i], maxWidth, "")
455 }
456
457 str = strings.Join(lines, "\n")
458 }
459
460 // Truncate according to MaxHeight
461 if maxHeight > 0 {
462 lines := strings.Split(str, "\n")
463 height := min(maxHeight, len(lines))
464 if len(lines) > 0 {
465 str = strings.Join(lines[:height], "\n")
466 }
467 }
468
469 return str
470}
471
472func (s Style) maybeConvertTabs(str string) string {
473 tw := tabWidthDefault
474 if s.isSet(tabWidthKey) {
475 tw = s.getAsInt(tabWidthKey)
476 }
477 switch tw {
478 case -1:
479 return str
480 case 0:
481 return strings.ReplaceAll(str, "\t", "")
482 default:
483 return strings.ReplaceAll(str, "\t", strings.Repeat(" ", tw))
484 }
485}
486
487func (s Style) applyMargins(str string, inline bool) string {
488 var (
489 topMargin = s.getAsInt(marginTopKey)
490 rightMargin = s.getAsInt(marginRightKey)
491 bottomMargin = s.getAsInt(marginBottomKey)
492 leftMargin = s.getAsInt(marginLeftKey)
493
494 styler termenv.Style
495 )
496
497 bgc := s.getAsColor(marginBackgroundKey)
498 if bgc != noColor {
499 styler = styler.Background(bgc.color(s.r))
500 }
501
502 // Add left and right margin
503 str = padLeft(str, leftMargin, &styler)
504 str = padRight(str, rightMargin, &styler)
505
506 // Top/bottom margin
507 if !inline {
508 _, width := getLines(str)
509 spaces := strings.Repeat(" ", width)
510
511 if topMargin > 0 {
512 str = styler.Styled(strings.Repeat(spaces+"\n", topMargin)) + str
513 }
514 if bottomMargin > 0 {
515 str += styler.Styled(strings.Repeat("\n"+spaces, bottomMargin))
516 }
517 }
518
519 return str
520}
521
522// Apply left padding.
523func padLeft(str string, n int, style *termenv.Style) string {
524 return pad(str, -n, style)
525}
526
527// Apply right padding.
528func padRight(str string, n int, style *termenv.Style) string {
529 return pad(str, n, style)
530}
531
532// pad adds padding to either the left or right side of a string.
533// Positive values add to the right side while negative values
534// add to the left side.
535func pad(str string, n int, style *termenv.Style) string {
536 if n == 0 {
537 return str
538 }
539
540 sp := strings.Repeat(" ", abs(n))
541 if style != nil {
542 sp = style.Styled(sp)
543 }
544
545 b := strings.Builder{}
546 l := strings.Split(str, "\n")
547
548 for i := range l {
549 switch {
550 // pad right
551 case n > 0:
552 b.WriteString(l[i])
553 b.WriteString(sp)
554 // pad left
555 default:
556 b.WriteString(sp)
557 b.WriteString(l[i])
558 }
559
560 if i != len(l)-1 {
561 b.WriteRune('\n')
562 }
563 }
564
565 return b.String()
566}
567
568func max(a, b int) int { //nolint:unparam,predeclared
569 if a > b {
570 return a
571 }
572 return b
573}
574
575func min(a, b int) int { //nolint:predeclared
576 if a < b {
577 return a
578 }
579 return b
580}
581
582func abs(a int) int {
583 if a < 0 {
584 return -a
585 }
586
587 return a
588}