1package lipgloss
2
3import (
4	"strings"
5	"unicode"
6
7	"github.com/muesli/reflow/truncate"
8	"github.com/muesli/reflow/wordwrap"
9	"github.com/muesli/reflow/wrap"
10	"github.com/muesli/termenv"
11)
12
13// Property for a key.
14type propKey int
15
16// Available properties.
17const (
18	boldKey propKey = iota
19	italicKey
20	underlineKey
21	strikethroughKey
22	reverseKey
23	blinkKey
24	faintKey
25	foregroundKey
26	backgroundKey
27	widthKey
28	heightKey
29	alignKey
30
31	// Padding.
32	paddingTopKey
33	paddingRightKey
34	paddingBottomKey
35	paddingLeftKey
36
37	colorWhitespaceKey
38
39	// Margins.
40	marginTopKey
41	marginRightKey
42	marginBottomKey
43	marginLeftKey
44	marginBackgroundKey
45
46	// Border runes.
47	borderStyleKey
48
49	// Border edges.
50	borderTopKey
51	borderRightKey
52	borderBottomKey
53	borderLeftKey
54
55	// Border foreground colors.
56	borderTopForegroundKey
57	borderRightForegroundKey
58	borderBottomForegroundKey
59	borderLeftForegroundKey
60
61	// Border background colors.
62	borderTopBackgroundKey
63	borderRightBackgroundKey
64	borderBottomBackgroundKey
65	borderLeftBackgroundKey
66
67	inlineKey
68	maxWidthKey
69	maxHeightKey
70	underlineSpacesKey
71	strikethroughSpacesKey
72)
73
74// A set of properties.
75type rules map[propKey]interface{}
76
77// NewStyle returns a new, empty Style.  While it's syntactic sugar for the
78// Style{} primitive, it's recommended to use this function for creating styles
79// incase the underlying implementation changes.
80func NewStyle() Style {
81	return Style{}
82}
83
84// Style contains a set of rules that comprise a style as a whole.
85type Style struct {
86	rules map[propKey]interface{}
87	value string
88}
89
90// SetString sets the underlying string value for this style. To render once
91// the underlying string is set, use the Style.String. This method is
92// a convenience for cases when having a stringer implementation is handy, such
93// as when using fmt.Sprintf. You can also simply define a style and render out
94// strings directly with Style.Render.
95func (s Style) SetString(str string) Style {
96	s.value = str
97	return s
98}
99
100// String implements stringer for a Style, returning the rendered result based
101// on the rules in this style. An underlying string value must be set with
102// Style.SetString prior to using this method.
103func (s Style) String() string {
104	return s.Render(s.value)
105}
106
107// Copy returns a copy of this style, including any underlying string values.
108func (s Style) Copy() Style {
109	o := NewStyle()
110	o.init()
111	for k, v := range s.rules {
112		o.rules[k] = v
113	}
114	o.value = s.value
115	return o
116}
117
118// Inherit takes values from the style in the argument applies them to this
119// style, overwriting existing definitions. Only values explicitly set on the
120// style in argument will be applied.
121//
122// Margins, padding, and underlying string values are not inherited.
123func (s Style) Inherit(i Style) Style {
124	s.init()
125
126	for k, v := range i.rules {
127		switch k {
128		case marginTopKey, marginRightKey, marginBottomKey, marginLeftKey:
129			// Margins are not inherited
130			continue
131		case paddingTopKey, paddingRightKey, paddingBottomKey, paddingLeftKey:
132			// Padding is not inherited
133			continue
134		case backgroundKey:
135			s.rules[k] = v
136
137			// The margins also inherit the background color
138			if !s.isSet(marginBackgroundKey) && !i.isSet(marginBackgroundKey) {
139				s.rules[marginBackgroundKey] = v
140			}
141		}
142
143		if _, exists := s.rules[k]; exists {
144			continue
145		}
146		s.rules[k] = v
147	}
148	return s
149}
150
151// Render applies the defined style formatting to a given string.
152func (s Style) Render(str string) string {
153	var (
154		te           termenv.Style
155		teSpace      termenv.Style
156		teWhitespace termenv.Style
157
158		bold          = s.getAsBool(boldKey, false)
159		italic        = s.getAsBool(italicKey, false)
160		underline     = s.getAsBool(underlineKey, false)
161		strikethrough = s.getAsBool(strikethroughKey, false)
162		reverse       = s.getAsBool(reverseKey, false)
163		blink         = s.getAsBool(blinkKey, false)
164		faint         = s.getAsBool(faintKey, false)
165
166		fg = s.getAsColor(foregroundKey)
167		bg = s.getAsColor(backgroundKey)
168
169		width  = s.getAsInt(widthKey)
170		height = s.getAsInt(heightKey)
171		align  = s.getAsPosition(alignKey)
172
173		topPadding    = s.getAsInt(paddingTopKey)
174		rightPadding  = s.getAsInt(paddingRightKey)
175		bottomPadding = s.getAsInt(paddingBottomKey)
176		leftPadding   = s.getAsInt(paddingLeftKey)
177
178		colorWhitespace = s.getAsBool(colorWhitespaceKey, true)
179		inline          = s.getAsBool(inlineKey, false)
180		maxWidth        = s.getAsInt(maxWidthKey)
181		maxHeight       = s.getAsInt(maxHeightKey)
182
183		underlineSpaces     = underline && s.getAsBool(underlineSpacesKey, true)
184		strikethroughSpaces = strikethrough && s.getAsBool(strikethroughSpacesKey, true)
185
186		// Do we need to style whitespace (padding and space outside
187		// paragraphs) separately?
188		styleWhitespace = reverse
189
190		// Do we need to style spaces separately?
191		useSpaceStyler = underlineSpaces || strikethroughSpaces
192	)
193
194	if len(s.rules) == 0 {
195		return str
196	}
197
198	// Enable support for ANSI on the legacy Windows cmd.exe console. This is a
199	// no-op on non-Windows systems and on Windows runs only once.
200	enableLegacyWindowsANSI()
201
202	if bold {
203		te = te.Bold()
204	}
205	if italic {
206		te = te.Italic()
207	}
208	if underline {
209		te = te.Underline()
210	}
211	if reverse {
212		if reverse {
213			teWhitespace = teWhitespace.Reverse()
214		}
215		te = te.Reverse()
216	}
217	if blink {
218		te = te.Blink()
219	}
220	if faint {
221		te = te.Faint()
222	}
223
224	if fg != noColor {
225		fgc := fg.color()
226		te = te.Foreground(fgc)
227		if styleWhitespace {
228			teWhitespace = teWhitespace.Foreground(fgc)
229		}
230		if useSpaceStyler {
231			teSpace = teSpace.Foreground(fgc)
232		}
233	}
234
235	if bg != noColor {
236		bgc := bg.color()
237		te = te.Background(bgc)
238		if colorWhitespace {
239			teWhitespace = teWhitespace.Background(bgc)
240		}
241		if useSpaceStyler {
242			teSpace = teSpace.Background(bgc)
243		}
244	}
245
246	if underline {
247		te = te.Underline()
248	}
249	if strikethrough {
250		te = te.CrossOut()
251	}
252
253	if underlineSpaces {
254		teSpace = teSpace.Underline()
255	}
256	if strikethroughSpaces {
257		teSpace = teSpace.CrossOut()
258	}
259
260	// Strip newlines in single line mode
261	if inline {
262		str = strings.Replace(str, "\n", "", -1)
263	}
264
265	// Word wrap
266	if !inline && width > 0 {
267		wrapAt := width - leftPadding - rightPadding
268		str = wordwrap.String(str, wrapAt)
269		str = wrap.String(str, wrapAt) // force-wrap long strings
270	}
271
272	// Render core text
273	{
274		var b strings.Builder
275
276		l := strings.Split(str, "\n")
277		for i := range l {
278			if useSpaceStyler {
279				// Look for spaces and apply a different styler
280				for _, r := range l[i] {
281					if unicode.IsSpace(r) {
282						b.WriteString(teSpace.Styled(string(r)))
283						continue
284					}
285					b.WriteString(te.Styled(string(r)))
286				}
287			} else {
288				b.WriteString(te.Styled(l[i]))
289			}
290			if i != len(l)-1 {
291				b.WriteRune('\n')
292			}
293		}
294
295		str = b.String()
296	}
297
298	// Padding
299	if !inline {
300		if leftPadding > 0 {
301			var st *termenv.Style
302			if colorWhitespace || styleWhitespace {
303				st = &teWhitespace
304			}
305			str = padLeft(str, leftPadding, st)
306		}
307
308		if rightPadding > 0 {
309			var st *termenv.Style
310			if colorWhitespace || styleWhitespace {
311				st = &teWhitespace
312			}
313			str = padRight(str, rightPadding, st)
314		}
315
316		if topPadding > 0 {
317			str = strings.Repeat("\n", topPadding) + str
318		}
319
320		if bottomPadding > 0 {
321			str += strings.Repeat("\n", bottomPadding)
322		}
323	}
324
325	// Height
326	if height > 0 {
327		h := strings.Count(str, "\n") + 1
328		if height > h {
329			str += strings.Repeat("\n", height-h)
330		}
331	}
332
333	// Set alignment. This will also pad short lines with spaces so that all
334	// lines are the same length, so we run it under a few different conditions
335	// beyond alignment.
336	{
337		numLines := strings.Count(str, "\n")
338
339		if !(numLines == 0 && width == 0) {
340			var st *termenv.Style
341			if colorWhitespace || styleWhitespace {
342				st = &teWhitespace
343			}
344			str = alignText(str, align, width, st)
345		}
346	}
347
348	if !inline {
349		str = s.applyBorder(str)
350		str = s.applyMargins(str, inline)
351	}
352
353	// Truncate according to MaxWidth
354	if maxWidth > 0 {
355		lines := strings.Split(str, "\n")
356
357		for i := range lines {
358			lines[i] = truncate.String(lines[i], uint(maxWidth))
359		}
360
361		str = strings.Join(lines, "\n")
362	}
363
364	// Truncate according to MaxHeight
365	if maxHeight > 0 {
366		lines := strings.Split(str, "\n")
367		str = strings.Join(lines[:min(maxHeight, len(lines))], "\n")
368	}
369
370	return str
371}
372
373func (s Style) applyMargins(str string, inline bool) string {
374	var (
375		topMargin    = s.getAsInt(marginTopKey)
376		rightMargin  = s.getAsInt(marginRightKey)
377		bottomMargin = s.getAsInt(marginBottomKey)
378		leftMargin   = s.getAsInt(marginLeftKey)
379
380		styler termenv.Style
381	)
382
383	bgc := s.getAsColor(marginBackgroundKey)
384	if bgc != noColor {
385		styler = styler.Background(bgc.color())
386	}
387
388	// Add left and right margin
389	str = padLeft(str, leftMargin, &styler)
390	str = padRight(str, rightMargin, &styler)
391
392	// Top/bottom margin
393	if !inline {
394		_, width := getLines(str)
395		spaces := strings.Repeat(" ", width)
396
397		if topMargin > 0 {
398			str = styler.Styled(strings.Repeat(spaces+"\n", topMargin)) + str
399		}
400		if bottomMargin > 0 {
401			str += styler.Styled(strings.Repeat("\n"+spaces, bottomMargin))
402		}
403	}
404
405	return str
406}
407
408// Apply left padding.
409func padLeft(str string, n int, style *termenv.Style) string {
410	if n == 0 {
411		return str
412	}
413
414	sp := strings.Repeat(" ", n)
415	if style != nil {
416		sp = style.Styled(sp)
417	}
418
419	b := strings.Builder{}
420	l := strings.Split(str, "\n")
421
422	for i := range l {
423		b.WriteString(sp)
424		b.WriteString(l[i])
425		if i != len(l)-1 {
426			b.WriteRune('\n')
427		}
428	}
429
430	return b.String()
431}
432
433// Apply right right padding.
434func padRight(str string, n int, style *termenv.Style) string {
435	if n == 0 || str == "" {
436		return str
437	}
438
439	sp := strings.Repeat(" ", n)
440	if style != nil {
441		sp = style.Styled(sp)
442	}
443
444	b := strings.Builder{}
445	l := strings.Split(str, "\n")
446
447	for i := range l {
448		b.WriteString(l[i])
449		b.WriteString(sp)
450		if i != len(l)-1 {
451			b.WriteRune('\n')
452		}
453	}
454
455	return b.String()
456}
457
458func max(a, b int) int {
459	if a > b {
460		return a
461	}
462	return b
463}
464
465func min(a, b int) int {
466	if a < b {
467		return a
468	}
469	return b
470}
471