hub/ui/format.go

265 строки
5.5 KiB
Go

package ui
import (
"regexp"
"strconv"
"strings"
)
// Expand expands a format string using `git log` message syntax.
func Expand(format string, values map[string]string, colorize bool) string {
f := &expander{values: values, colorize: colorize}
return f.Expand(format)
}
// An expander is a stateful helper to expand a format string.
type expander struct {
// formatted holds the parts of the string that have already been formatted.
formatted []string
// values is the map of values that should be expanded.
values map[string]string
// colorize is a flag to indiciate whether to use colors.
colorize bool
// skipNext is true if the next placeholder is not a placeholder and can be
// output directly as such.
skipNext bool
// padNext is an object that should be used to pad the next placeholder.
padNext *padder
}
func (f *expander) Expand(format string) string {
parts := strings.Split(format, "%")
f.formatted = make([]string, 0, len(parts))
f.append(parts[0])
for _, p := range parts[1:] {
v, t := f.expandOneVar(p)
f.append(v, t)
}
return f.crush()
}
func (f *expander) append(formattedText ...string) {
f.formatted = append(f.formatted, formattedText...)
}
func (f *expander) crush() string {
s := strings.Join(f.formatted, "")
f.formatted = nil
return s
}
var colorMap = map[string]string{
"black": "30",
"red": "31",
"green": "32",
"yellow": "33",
"blue": "34",
"magenta": "35",
"cyan": "36",
"white": "37",
"reset": "",
}
func (f *expander) expandOneVar(format string) (expand string, untouched string) {
if f.skipNext {
f.skipNext = false
return "", format
}
if format == "" {
f.skipNext = true
return "", "%"
}
if f.padNext != nil {
p := f.padNext
f.padNext = nil
e, u := f.expandOneVar(format)
return f.pad(e, p), u
}
if e, u, ok := f.expandSpecialChar(format[0], format[1:]); ok {
return e, u
}
if f.values != nil {
for i := 1; i <= len(format); i++ {
if v, exists := f.values[format[0:i]]; exists {
return v, format[i:]
}
}
}
return "", "%" + format
}
func (f *expander) expandSpecialChar(firstChar byte, format string) (expand string, untouched string, wasExpanded bool) {
switch firstChar {
case 'n':
return "\n", format, true
case 'C':
for k, v := range colorMap {
if strings.HasPrefix(format, k) {
if f.colorize {
return "\033[" + v + "m", format[len(k):], true
}
return "", format[len(k):], true
}
}
// TODO: Add custom color as specified in color.branch.* options.
// TODO: Handle auto-coloring.
case 'x':
if len(format) >= 2 {
if v, err := strconv.ParseInt(format[:2], 16, 32); err == nil {
return string(v), format[2:], true
}
}
case '+':
if e, u := f.expandOneVar(format); e != "" {
return "\n" + e, u, true
} else {
return "", u, true
}
case ' ':
if e, u := f.expandOneVar(format); e != "" {
return " " + e, u, true
} else {
return "", u, true
}
case '-':
if e, u := f.expandOneVar(format); e != "" {
return e, u, true
} else {
f.append(strings.TrimRight(f.crush(), "\n"))
return "", u, true
}
case '<', '>':
if m := paddingPattern.FindStringSubmatch(string(firstChar) + format); len(m) == 7 {
if p := padderFromConfig(m[1], m[2], m[3], m[4], m[5]); p != nil {
f.padNext = p
return "", m[6], true
}
}
}
return "", "", false
}
func (f *expander) pad(s string, p *padder) string {
size := int(p.size)
if p.sizeAsColumn {
previous := f.crush()
f.append(previous)
size -= len(previous) - strings.LastIndex(previous, "\n") - 1
}
numPadding := size - len(s)
if numPadding == 0 {
return s
}
if numPadding < 0 {
if p.usePreviousSpace {
previous := f.crush()
noBlanks := strings.TrimRight(previous, " ")
f.append(noBlanks)
numPadding += len(previous) - len(noBlanks)
}
if numPadding <= 0 {
return p.truncate(s, -numPadding)
}
}
switch p.orientation {
case padLeft:
return strings.Repeat(" ", numPadding) + s
case padMiddle:
return strings.Repeat(" ", numPadding/2) + s + strings.Repeat(" ", (numPadding+1)/2)
}
// Pad right by default.
return s + strings.Repeat(" ", numPadding)
}
type paddingOrientation int
const (
padRight paddingOrientation = iota
padLeft
padMiddle
)
type truncingMethod int
const (
noTrunc truncingMethod = iota
truncLeft
truncRight
truncMiddle
)
type padder struct {
orientation paddingOrientation
size int64
sizeAsColumn bool
usePreviousSpace bool
truncing truncingMethod
}
var paddingPattern = regexp.MustCompile(`^(>)?([><])(\|)?\((\d+)(,[rm]?trunc)?\)(.*)$`)
func padderFromConfig(alsoLeft, orientation, asColumn, size, trunc string) *padder {
p := &padder{}
if orientation == ">" {
p.orientation = padLeft
} else if alsoLeft == "" {
p.orientation = padRight
} else {
p.orientation = padMiddle
}
p.sizeAsColumn = asColumn != ""
var err error
if p.size, err = strconv.ParseInt(size, 10, 64); err != nil {
return nil
}
p.usePreviousSpace = alsoLeft != "" && p.orientation == padLeft
switch trunc {
case ",trunc":
p.truncing = truncLeft
case ",rtrunc":
p.truncing = truncRight
case ",mtrunc":
p.truncing = truncMiddle
}
return p
}
func (p *padder) truncate(s string, numReduce int) string {
if numReduce == 0 {
return s
}
numLeft := len(s) - numReduce - 2
if numLeft < 0 {
numLeft = 0
}
switch p.truncing {
case truncRight:
return ".." + s[len(s)-numLeft:len(s)]
case truncMiddle:
return s[:numLeft/2] + ".." + s[len(s)-(numLeft+1)/2:len(s)]
}
// Trunc left by default.
return s[:numLeft] + ".."
}