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