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 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}