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