File: template.go

package info (click to toggle)
golang-github-containers-common 0.50.1%2Bds1-4
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 4,440 kB
  • sloc: makefile: 118; sh: 46
file content (175 lines) | stat: -rw-r--r-- 4,904 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
package report

import (
	"bytes"
	"encoding/json"
	"reflect"
	"regexp"
	"strings"
	"text/template"

	"github.com/containers/common/pkg/report/camelcase"
)

// Template embeds template.Template to add functionality to methods
type Template struct {
	*template.Template
	isTable bool
}

// FuncMap is aliased from template.FuncMap
type FuncMap template.FuncMap

// tableReplacer will remove 'table ' prefix and clean up tabs
var tableReplacer = strings.NewReplacer(
	"table ", "",
	`\t`, "\t",
	" ", "\t",
	`\n`, "\n",
)

// escapedReplacer will clean up escaped characters from CLI
var escapedReplacer = strings.NewReplacer(
	`\t`, "\t",
	`\n`, "\n",
)

var DefaultFuncs = FuncMap{
	"join": strings.Join,
	"json": func(v interface{}) string {
		buf := new(bytes.Buffer)
		enc := json.NewEncoder(buf)
		enc.SetEscapeHTML(false)
		_ = enc.Encode(v)
		// Remove the trailing new line added by the encoder
		return strings.TrimSpace(buf.String())
	},
	"lower":    strings.ToLower,
	"pad":      padWithSpace,
	"split":    strings.Split,
	"title":    strings.Title, //nolint:staticcheck
	"truncate": truncateWithLength,
	"upper":    strings.ToUpper,
}

// NormalizeFormat reads given go template format provided by CLI and munges it into what we need
func NormalizeFormat(format string) string {
	var f string
	// two replacers used so we only remove the prefix keyword `table`
	if strings.HasPrefix(format, "table ") {
		f = tableReplacer.Replace(format)
	} else {
		f = escapedReplacer.Replace(format)
	}

	if !strings.HasSuffix(f, "\n") {
		f += "\n"
	}
	return f
}

// padWithSpace adds spaces*prefix and spaces*suffix to the input when it is non-empty
func padWithSpace(source string, prefix, suffix int) string {
	if source == "" {
		return source
	}
	return strings.Repeat(" ", prefix) + source + strings.Repeat(" ", suffix)
}

// truncateWithLength truncates the source string up to the length provided by the input
func truncateWithLength(source string, length int) string {
	if len(source) < length {
		return source
	}
	return source[:length]
}

// Headers queries the interface for field names.
// Array of map is returned to support range templates
// Note: unexported fields can be supported by adding field to overrides
// Note: It is left to the developer to write out said headers
//       Podman commands use the general rules of:
//       1) unchanged --format includes headers
//       2) --format '{{.ID}"        # no headers
//       3) --format 'table {{.ID}}' # includes headers
func Headers(object interface{}, overrides map[string]string) []map[string]string {
	value := reflect.ValueOf(object)
	if value.Kind() == reflect.Ptr {
		value = value.Elem()
	}

	// Column header will be field name upper-cased.
	headers := make(map[string]string, value.NumField())
	for i := 0; i < value.Type().NumField(); i++ {
		field := value.Type().Field(i)
		// Recurse to find field names from promoted structs
		if field.Type.Kind() == reflect.Struct && field.Anonymous {
			h := Headers(reflect.New(field.Type).Interface(), nil)
			for k, v := range h[0] {
				headers[k] = v
			}
			continue
		}
		name := strings.Join(camelcase.Split(field.Name), " ")
		headers[field.Name] = strings.ToUpper(name)
	}

	if len(overrides) > 0 {
		// Override column header as provided
		for k, v := range overrides {
			headers[k] = strings.ToUpper(v)
		}
	}
	return []map[string]string{headers}
}

// NewTemplate creates a new template object
func NewTemplate(name string) *Template {
	return &Template{Template: template.New(name).Funcs(template.FuncMap(DefaultFuncs))}
}

// Parse parses text as a template body for t
func (t *Template) Parse(text string) (*Template, error) {
	if strings.HasPrefix(text, "table ") {
		t.isTable = true
		text = "{{range .}}" + NormalizeFormat(text) + "{{end -}}"
	} else {
		text = NormalizeFormat(text)
	}

	tt, err := t.Template.Funcs(template.FuncMap(DefaultFuncs)).Parse(text)
	return &Template{tt, t.isTable}, err
}

// Funcs adds the elements of the argument map to the template's function map.
// A default template function will be replace if there is a key collision.
func (t *Template) Funcs(funcMap FuncMap) *Template {
	m := make(FuncMap)
	for k, v := range DefaultFuncs {
		m[k] = v
	}
	for k, v := range funcMap {
		m[k] = v
	}
	return &Template{Template: t.Template.Funcs(template.FuncMap(m)), isTable: t.isTable}
}

// IsTable returns true if format string defines a "table"
func (t *Template) IsTable() bool {
	return t.isTable
}

var rangeRegex = regexp.MustCompile(`(?s){{\s*range\s*\.\s*}}.*{{\s*end\s*-?\s*}}`)

// EnforceRange ensures that the format string contains a range
func EnforceRange(format string) string {
	if !rangeRegex.MatchString(format) {
		return "{{range .}}" + format + "{{end -}}"
	}
	return format
}

// HasTable returns whether the format is a table
func HasTable(format string) bool {
	return strings.HasPrefix(format, "table ")
}