style.go

  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}