main
Raw Download raw file
  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}