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