Skip to content

Commit 05415b9

Browse files
authored
Allow defining template user functions (#74)
1 parent 13c66f3 commit 05415b9

File tree

2 files changed

+93
-0
lines changed

2 files changed

+93
-0
lines changed

pkg/template/template.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ type Template struct {
2929
tmpl *template.Template
3030
tp tableprinter.TablePrinter
3131
width int
32+
funcs template.FuncMap
3233
}
3334

3435
// New initializes a Template.
@@ -38,9 +39,21 @@ func New(w io.Writer, width int, colorEnabled bool) Template {
3839
output: w,
3940
tp: tableprinter.New(w, true, width),
4041
width: width,
42+
funcs: template.FuncMap{},
4143
}
4244
}
4345

46+
// Funcs adds the elements of the argument map to the template's function map.
47+
// It must be called before the template is parsed.
48+
// It is legal to overwrite elements of the map including default functions.
49+
// The return value is the template, so calls can be chained.
50+
func (t *Template) Funcs(funcMap map[string]interface{}) *Template {
51+
for name, f := range funcMap {
52+
t.funcs[name] = f
53+
}
54+
return t
55+
}
56+
4457
// Parse the given template string for use with Execute.
4558
func (t *Template) Parse(tmpl string) error {
4659
now := time.Now()
@@ -70,6 +83,9 @@ func (t *Template) Parse(tmpl string) error {
7083
if !t.colorEnabled {
7184
templateFuncs["autocolor"] = autoColorFunc
7285
}
86+
for name, f := range t.funcs {
87+
templateFuncs[name] = f
88+
}
7389
var err error
7490
t.tmpl, err = template.New("").Funcs(templateFuncs).Parse(tmpl)
7591
return err

pkg/template/template_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"time"
1212

1313
"github.com/MakeNowJust/heredoc"
14+
"github.com/cli/go-gh/pkg/text"
1415
"github.com/stretchr/testify/assert"
1516
)
1617

@@ -39,6 +40,44 @@ func ExampleTemplate() {
3940
// FOOTER
4041
}
4142

43+
func ExampleTemplate_Funcs() {
44+
// Information about the terminal can be obtained using the [pkg/term] package.
45+
colorEnabled := true
46+
termWidth := 14
47+
json := strings.NewReader(heredoc.Doc(`[
48+
{"num": 1, "thing": "apple"},
49+
{"num": 2, "thing": "orange"}
50+
]`))
51+
template := "{{range .}}* {{pluralize .num .thing}}\n{{end}}"
52+
tmpl := New(os.Stdout, termWidth, colorEnabled)
53+
tmpl.Funcs(map[string]interface{}{
54+
"pluralize": func(fields ...interface{}) (string, error) {
55+
if l := len(fields); l != 2 {
56+
return "", fmt.Errorf("wrong number of args for pluralize: want 2 got %d", l)
57+
}
58+
var ok bool
59+
var num float64
60+
var thing string
61+
if num, ok = fields[0].(float64); !ok && num == float64(int(num)) {
62+
return "", fmt.Errorf("invalid value; expected int")
63+
}
64+
if thing, ok = fields[1].(string); !ok {
65+
return "", fmt.Errorf("invalid value; expected string")
66+
}
67+
return text.Pluralize(int(num), thing), nil
68+
},
69+
})
70+
if err := tmpl.Parse(template); err != nil {
71+
log.Fatal(err)
72+
}
73+
if err := tmpl.Execute(json); err != nil {
74+
log.Fatal(err)
75+
}
76+
// Output:
77+
// * 1 apple
78+
// * 2 oranges
79+
}
80+
4281
func TestJsonScalarToString(t *testing.T) {
4382
tests := []struct {
4483
name string
@@ -432,3 +471,41 @@ func TestTruncateMultiline(t *testing.T) {
432471
})
433472
}
434473
}
474+
475+
func TestFuncs(t *testing.T) {
476+
w := &bytes.Buffer{}
477+
tmpl := New(w, 80, false)
478+
479+
// Override "truncate" and define a new "foo" function.
480+
tmpl.Funcs(map[string]interface{}{
481+
"truncate": func(fields ...interface{}) (string, error) {
482+
if l := len(fields); l != 2 {
483+
return "", fmt.Errorf("wrong number of args for truncate: want 2 got %d", l)
484+
}
485+
var ok bool
486+
var width int
487+
var input string
488+
if width, ok = fields[0].(int); !ok {
489+
return "", fmt.Errorf("invalid value; expected int")
490+
}
491+
if input, ok = fields[1].(string); !ok {
492+
return "", fmt.Errorf("invalid value; expected string")
493+
}
494+
return input[:width], nil
495+
},
496+
"foo": func(fields ...interface{}) (string, error) {
497+
return "test", nil
498+
},
499+
})
500+
501+
err := tmpl.Parse(`{{ .text | truncate 5 }} {{ .status | color "green" }} {{ foo }}`)
502+
assert.NoError(t, err)
503+
504+
r := strings.NewReader(`{"text":"truncated","status":"open"}`)
505+
err = tmpl.Execute(r)
506+
assert.NoError(t, err)
507+
508+
err = tmpl.Flush()
509+
assert.NoError(t, err)
510+
assert.Equal(t, "trunc \x1b[0;32mopen\x1b[0m test", w.String())
511+
}

0 commit comments

Comments
 (0)