1package ui
2
3import (
4	"regexp"
5	"strconv"
6	"strings"
7)
8
9// Expand expands a format string using `git log` message syntax.
10func Expand(format string, values map[string]string, colorize bool) string {
11	f := &expander{values: values, colorize: colorize}
12	return f.Expand(format)
13}
14
15// An expander is a stateful helper to expand a format string.
16type expander struct {
17	// formatted holds the parts of the string that have already been formatted.
18	formatted []string
19
20	// values is the map of values that should be expanded.
21	values map[string]string
22
23	// colorize is a flag to indicate whether to use colors.
24	colorize bool
25
26	// skipNext is true if the next placeholder is not a placeholder and can be
27	// output directly as such.
28	skipNext bool
29
30	// padNext is an object that should be used to pad the next placeholder.
31	padNext *padder
32}
33
34func (f *expander) Expand(format string) string {
35	parts := strings.Split(format, "%")
36	f.formatted = make([]string, 0, len(parts))
37	f.append(parts[0])
38	for _, p := range parts[1:] {
39		v, t := f.expandOneVar(p)
40		f.append(v, t)
41	}
42	return f.crush()
43}
44
45func (f *expander) append(formattedText ...string) {
46	f.formatted = append(f.formatted, formattedText...)
47}
48
49func (f *expander) crush() string {
50	s := strings.Join(f.formatted, "")
51	f.formatted = nil
52	return s
53}
54
55var colorMap = map[string]string{
56	"black":   "30",
57	"red":     "31",
58	"green":   "32",
59	"yellow":  "33",
60	"blue":    "34",
61	"magenta": "35",
62	"cyan":    "36",
63	"white":   "37",
64	"reset":   "",
65}
66
67func (f *expander) expandOneVar(format string) (expand string, untouched string) {
68	if f.skipNext {
69		f.skipNext = false
70		return "", format
71	}
72	if format == "" {
73		f.skipNext = true
74		return "", "%"
75	}
76
77	if f.padNext != nil {
78		p := f.padNext
79		f.padNext = nil
80		e, u := f.expandOneVar(format)
81		return f.pad(e, p), u
82	}
83
84	if e, u, ok := f.expandSpecialChar(format[0], format[1:]); ok {
85		return e, u
86	}
87
88	if f.values != nil {
89		for i := 1; i <= len(format); i++ {
90			if v, exists := f.values[format[0:i]]; exists {
91				return v, format[i:]
92			}
93		}
94	}
95
96	return "", "%" + format
97}
98
99func (f *expander) expandSpecialChar(firstChar byte, format string) (expand string, untouched string, wasExpanded bool) {
100	switch firstChar {
101	case 'n':
102		return "\n", format, true
103	case 'C':
104		for k, v := range colorMap {
105			if strings.HasPrefix(format, k) {
106				if f.colorize {
107					return "\033[" + v + "m", format[len(k):], true
108				}
109				return "", format[len(k):], true
110			}
111		}
112		// TODO: Add custom color as specified in color.branch.* options.
113		// TODO: Handle auto-coloring.
114	case 'x':
115		if len(format) >= 2 {
116			if v, err := strconv.ParseInt(format[:2], 16, 32); err == nil {
117				return string(v), format[2:], true
118			}
119		}
120	case '+':
121		if e, u := f.expandOneVar(format); e != "" {
122			return "\n" + e, u, true
123		} else {
124			return "", u, true
125		}
126	case ' ':
127		if e, u := f.expandOneVar(format); e != "" {
128			return " " + e, u, true
129		} else {
130			return "", u, true
131		}
132	case '-':
133		if e, u := f.expandOneVar(format); e != "" {
134			return e, u, true
135		} else {
136			f.append(strings.TrimRight(f.crush(), "\n"))
137			return "", u, true
138		}
139	case '<', '>':
140		if m := paddingPattern.FindStringSubmatch(string(firstChar) + format); len(m) == 7 {
141			if p := padderFromConfig(m[1], m[2], m[3], m[4], m[5]); p != nil {
142				f.padNext = p
143				return "", m[6], true
144			}
145		}
146	}
147	return "", "", false
148}
149
150func (f *expander) pad(s string, p *padder) string {
151	size := int(p.size)
152	if p.sizeAsColumn {
153		previous := f.crush()
154		f.append(previous)
155		size -= len(previous) - strings.LastIndex(previous, "\n") - 1
156	}
157
158	numPadding := size - len(s)
159	if numPadding == 0 {
160		return s
161	}
162
163	if numPadding < 0 {
164		if p.usePreviousSpace {
165			previous := f.crush()
166			noBlanks := strings.TrimRight(previous, " ")
167			f.append(noBlanks)
168			numPadding += len(previous) - len(noBlanks)
169		}
170
171		if numPadding <= 0 {
172			return p.truncate(s, -numPadding)
173		}
174	}
175
176	switch p.orientation {
177	case padLeft:
178		return strings.Repeat(" ", numPadding) + s
179	case padMiddle:
180		return strings.Repeat(" ", numPadding/2) + s + strings.Repeat(" ", (numPadding+1)/2)
181	}
182
183	// Pad right by default.
184	return s + strings.Repeat(" ", numPadding)
185}
186
187type paddingOrientation int
188
189const (
190	padRight paddingOrientation = iota
191	padLeft
192	padMiddle
193)
194
195type truncingMethod int
196
197const (
198	noTrunc truncingMethod = iota
199	truncLeft
200	truncRight
201	truncMiddle
202)
203
204type padder struct {
205	orientation      paddingOrientation
206	size             int64
207	sizeAsColumn     bool
208	usePreviousSpace bool
209	truncing         truncingMethod
210}
211
212var paddingPattern = regexp.MustCompile(`^(>)?([><])(\|)?\((\d+)(,[rm]?trunc)?\)(.*)$`)
213
214func padderFromConfig(alsoLeft, orientation, asColumn, size, trunc string) *padder {
215	p := &padder{}
216
217	if orientation == ">" {
218		p.orientation = padLeft
219	} else if alsoLeft == "" {
220		p.orientation = padRight
221	} else {
222		p.orientation = padMiddle
223	}
224
225	p.sizeAsColumn = asColumn != ""
226
227	var err error
228	if p.size, err = strconv.ParseInt(size, 10, 64); err != nil {
229		return nil
230	}
231
232	p.usePreviousSpace = alsoLeft != "" && p.orientation == padLeft
233
234	switch trunc {
235	case ",trunc":
236		p.truncing = truncLeft
237	case ",rtrunc":
238		p.truncing = truncRight
239	case ",mtrunc":
240		p.truncing = truncMiddle
241	}
242
243	return p
244}
245
246func (p *padder) truncate(s string, numReduce int) string {
247	if numReduce == 0 {
248		return s
249	}
250	numLeft := len(s) - numReduce - 2
251	if numLeft < 0 {
252		numLeft = 0
253	}
254
255	switch p.truncing {
256	case truncRight:
257		return ".." + s[len(s)-numLeft:]
258	case truncMiddle:
259		return s[:numLeft/2] + ".." + s[len(s)-(numLeft+1)/2:]
260	}
261
262	// Trunc left by default.
263	return s[:numLeft] + ".."
264}
265