Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit c42d6eb

Browse files
committedMay 14, 2024·
chore: split and organize files
1 parent e3205f8 commit c42d6eb

16 files changed

+633
-517
lines changed
 

‎.github/workflows/main.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
- uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491
2121
- uses: golangci/golangci-lint-action@9d1e0624a798bb64f6c3cea93db47765312263dc
2222
with:
23-
version: v1.55.2
23+
version: v1.58.1
2424
- uses: DavidAnson/markdownlint-cli2-action@b4c9feab76d8025d1e83c653fa3990936df0e6c8
2525
with:
2626
globs: '**/*.md'

‎.golangci.yml

+2-3
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ linters:
1111
- dupl
1212
- dupword
1313
- durationcheck
14+
- err113
1415
- errcheck
1516
- errchkjson
1617
- errname
1718
- errorlint
18-
- execinquery
1919
- exhaustive
2020
- exportloopref
2121
- forbidigo
@@ -30,7 +30,6 @@ linters:
3030
- gocritic
3131
- gocyclo
3232
- godot
33-
- goerr113
3433
- gofmt
3534
- gofumpt
3635
- goheader
@@ -136,7 +135,7 @@ issues:
136135
- gosec
137136
path: "internal/"
138137
- linters:
139-
- goerr113
138+
- err113
140139
text: do not define dynamic errors, use wrapped static errors instead
141140
- linters:
142141
- gochecknoinits

‎docs/booleanfuncs.md

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Boolean Functions
2+
3+
## `eqFold` *string1* *string2* [*extraStrings*...]
4+
5+
`eqFold` returns the boolean truth of comparing *string1* with *string2*
6+
and any number of *extraStrings* under Unicode case-folding.
7+
8+
```text
9+
{{ eqFold "föö" "FOO" }}
10+
11+
true
12+
```

‎docs/conversionfuncs.md

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Conversion Functions
2+
3+
## `fromJSON` *jsontext*
4+
5+
`fromJSON` parses *jsontext* as JSON and returns the parsed value.
6+
7+
```text
8+
{{ `{ "foo": "bar" }` | fromJSON }}
9+
```
10+
11+
## `hexDecode` *hextext*
12+
13+
`hexDecode` returns the bytes represented by *hextext*.
14+
15+
```text
16+
{{ hexDecode "666f6f626172" }}
17+
18+
foobar
19+
```
20+
21+
## `hexEncode` *string*
22+
23+
`hexEncode` returns the hexadecimal encoding of *string*.
24+
25+
```text
26+
{{ hexEncode "foobar" }}
27+
28+
666f6f626172
29+
```
30+
31+
## `toJSON` *input*
32+
33+
`toJSON` returns a JSON string representation of *input*.
34+
35+
```text
36+
{{ list "foo" "bar" "baz" }}
37+
38+
["foo","bar","baz"]
39+
```
40+
41+
## `toString` *input*
42+
43+
`toString` returns the string representation of *input*.
44+
45+
```text
46+
{{ toString 10 }}
47+
```

‎docs/docs.go

+55-12
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,75 @@
11
package docs
22

33
import (
4-
_ "embed"
4+
"embed"
5+
"log"
6+
"maps"
57
"regexp"
68
"strings"
79
)
810

9-
//go:embed templatefuncs.md
10-
var templateFuncsStr string
11-
1211
type Reference struct {
12+
Type string
1313
Title string
1414
Body string
1515
Example string
1616
}
1717

18-
var References map[string]Reference
18+
//go:embed *.md
19+
var f embed.FS
1920

20-
func init() {
21-
newlineRx := regexp.MustCompile(`\r?\n`)
21+
var (
22+
References map[string]Reference
2223

24+
newlineRx = regexp.MustCompile(`\r?\n`)
25+
pageTitleRx = regexp.MustCompile(`^#\s+(\S+)`)
2326
// Template function names must start with a letter or underscore
2427
// and can subsequently contain letters, underscores and digits.
25-
funcNameRx := regexp.MustCompile("`" + `([a-zA-Z_]\w*)` + "`")
28+
funcNameRx = regexp.MustCompile("`" + `([a-zA-Z_]\w*)` + "`")
29+
)
2630

27-
References = make(map[string]Reference)
31+
func readFiles() []string {
32+
fileContents := []string{}
33+
34+
fileInfos, err := f.ReadDir(".")
35+
if err != nil {
36+
log.Fatal(err)
37+
}
38+
39+
for _, fileInfo := range fileInfos {
40+
if fileInfo.IsDir() || !strings.HasSuffix(fileInfo.Name(), ".md") {
41+
continue
42+
}
43+
content, err := f.ReadFile(fileInfo.Name())
44+
if err != nil {
45+
log.Fatal(err)
46+
}
47+
fileContents = append(fileContents, string(content))
48+
}
49+
50+
return fileContents
51+
}
52+
53+
func parseFile(file string) map[string]Reference {
54+
references := make(map[string]Reference)
2855
var reference Reference
2956
var funcName string
3057
var b strings.Builder
3158
var e strings.Builder
3259
inExample := false
3360

34-
for _, line := range newlineRx.Split(templateFuncsStr, -1) {
61+
lines := newlineRx.Split(file, -1)
62+
funcType, lines := pageTitleRx.FindStringSubmatch(lines[0])[1], lines[1:]
63+
64+
for _, line := range lines {
3565
switch {
3666
case strings.HasPrefix(line, "## "):
3767
if reference.Title != "" {
38-
References[funcName] = reference
68+
references[funcName] = reference
3969
}
4070
funcName = funcNameRx.FindStringSubmatch(line)[1]
4171
reference = Reference{}
72+
reference.Type = funcType
4273
reference.Title = strings.TrimPrefix(line, "## ")
4374
case strings.HasPrefix(line, "```"):
4475
if !inExample {
@@ -63,6 +94,18 @@ func init() {
6394
}
6495

6596
if reference.Title != "" {
66-
References[funcName] = reference
97+
references[funcName] = reference
98+
}
99+
100+
return references
101+
}
102+
103+
func init() {
104+
References = make(map[string]Reference)
105+
106+
files := readFiles()
107+
108+
for _, file := range files {
109+
maps.Copy(References, parseFile(file))
67110
}
68111
}

‎docs/docs_test.go

+11-6
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
func TestReferences(t *testing.T) {
1212
assert.Equal(t, docs.Reference{
13+
Type: "String",
1314
Title: "`contains` *substring* *string*",
1415
Body: "`contains` returns whether *substring* is in *string*.",
1516
Example: "" +
@@ -20,12 +21,16 @@ func TestReferences(t *testing.T) {
2021
"```",
2122
}, docs.References["contains"])
2223
assert.Equal(t, docs.Reference{
23-
Title: "`trimSpace` *string*",
24-
Body: "`trimSpace` returns *string* with all spaces removed.",
25-
Example: "```text\n" +
26-
"{{ \" foobar \" | trimSpace }}\n" +
24+
Type: "Boolean",
25+
Title: "`eqFold` *string1* *string2* [*extraStrings*...]",
26+
Body: "" +
27+
"`eqFold` returns the boolean truth of comparing *string1* with *string2*\n" +
28+
"and any number of *extraStrings* under Unicode case-folding.",
29+
Example: "" +
30+
"```text\n" +
31+
"{{ eqFold \"föö\" \"FOO\" }}\n" +
2732
"\n" +
28-
"foobar\n" +
33+
"true\n" +
2934
"```",
30-
}, docs.References["trimSpace"])
35+
}, docs.References["eqFold"])
3136
}

‎docs/listfuncs.md

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# List Functions
2+
3+
## `join` *delimiter* *list*
4+
5+
`join` returns a string containing each item in *list* joined with *delimiter*.
6+
7+
```text
8+
{{ list "foo" "bar" "baz" | join "," }}
9+
10+
foo,bar,baz
11+
```
12+
13+
## `list` *items*...
14+
15+
`list` creates a new list containing *items*.
16+
17+
```text
18+
{{ list "foo" "bar" "baz" }}
19+
```
20+
21+
## `prefixLines` *prefix* *list*
22+
23+
`prefixLines` returns a string consisting of each item in *list*
24+
with the prefix *prefix*.
25+
26+
```text
27+
{{ list "this is" "a multi-line" "comment" | prefixLines "# " }}
28+
29+
# this is
30+
# a multi-line
31+
# comment
32+
```

‎docs/stringfuncs.md

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# String Functions
2+
3+
## `contains` *substring* *string*
4+
5+
`contains` returns whether *substring* is in *string*.
6+
7+
```text
8+
{{ "abc" | contains "ab" }}
9+
10+
true
11+
```
12+
13+
## `hasPrefix` *prefix* *string*
14+
15+
`hasPrefix` returns whether *string* begins with *prefix*.
16+
17+
```text
18+
{{ "foobar" | hasPrefix "foo" }}
19+
20+
true
21+
```
22+
23+
## `hasSuffix` *suffix* *string*
24+
25+
`hasSuffix` returns whether *string* ends with *suffix*.
26+
27+
```text
28+
{{ "foobar" | hasSuffix "bar" }}
29+
30+
true
31+
```
32+
33+
## `quote` *input*
34+
35+
`quote` returns a double-quoted string literal containing *input*.
36+
*input* can be a string or list of strings.
37+
38+
```text
39+
{{ "foobar" | quote }}
40+
41+
"foobar"
42+
```
43+
44+
## `regexpReplaceAll` *pattern* *replacement* *string*
45+
46+
`regexpReplaceAll` replaces all instances of *pattern*
47+
with *replacement* in *string*.
48+
49+
```text
50+
{{ "foobar" | regexpReplaceAll "o*b" "" }}
51+
52+
far
53+
```
54+
55+
## `toLower` *string*
56+
57+
`toLower` returns *string* with all letters converted to lower case.
58+
59+
```text
60+
{{ toLower "FOOBAR" }}
61+
62+
foobar
63+
```
64+
65+
## `toUpper` *string*
66+
67+
`toUpper` returns *string* with all letters converted to upper case.
68+
69+
```text
70+
{{ toUpper "foobar" }}
71+
72+
FOOBAR
73+
```
74+
75+
## `trimSpace` *string*
76+
77+
`trimSpace` returns *string* with all spaces removed.
78+
79+
```text
80+
{{ " foobar " | trimSpace }}
81+
82+
foobar
83+
```

‎docs/templatefuncs.md

-170
Original file line numberDiff line numberDiff line change
@@ -1,92 +1,5 @@
11
# Template Functions
22

3-
## `contains` *substring* *string*
4-
5-
`contains` returns whether *substring* is in *string*.
6-
7-
```text
8-
{{ "abc" | contains "ab" }}
9-
10-
true
11-
```
12-
13-
## `eqFold` *string1* *string2* [*extraStrings*...]
14-
15-
`eqFold` returns the boolean truth of comparing *string1* with *string2*
16-
and any number of *extraStrings* under Unicode case-folding.
17-
18-
```text
19-
{{ eqFold "föö" "FOO" }}
20-
21-
true
22-
```
23-
24-
## `fromJSON` *jsontext*
25-
26-
`fromJSON` parses *jsontext* as JSON and returns the parsed value.
27-
28-
```text
29-
{{ `{ "foo": "bar" }` | fromJSON }}
30-
```
31-
32-
## `hasPrefix` *prefix* *string*
33-
34-
`hasPrefix` returns whether *string* begins with *prefix*.
35-
36-
```text
37-
{{ "foobar" | hasPrefix "foo" }}
38-
39-
true
40-
```
41-
42-
## `hasSuffix` *suffix* *string*
43-
44-
`hasSuffix` returns whether *string* ends with *suffix*.
45-
46-
```text
47-
{{ "foobar" | hasSuffix "bar" }}
48-
49-
true
50-
```
51-
52-
## `hexDecode` *hextext*
53-
54-
`hexDecode` returns the bytes represented by *hextext*.
55-
56-
```text
57-
{{ hexDecode "666f6f626172" }}
58-
59-
foobar
60-
```
61-
62-
## `hexEncode` *string*
63-
64-
`hexEncode` returns the hexadecimal encoding of *string*.
65-
66-
```text
67-
{{ hexEncode "foobar" }}
68-
69-
666f6f626172
70-
```
71-
72-
## `join` *delimiter* *list*
73-
74-
`join` returns a string containing each item in *list* joined with *delimiter*.
75-
76-
```text
77-
{{ list "foo" "bar" "baz" | join "," }}
78-
79-
foo,bar,baz
80-
```
81-
82-
## `list` *items*...
83-
84-
`list` creates a new list containing *items*.
85-
86-
```text
87-
{{ list "foo" "bar" "baz" }}
88-
```
89-
903
## `lookPath` *file*
914

925
`lookPath` searches for the executable *file* in the users `PATH`
@@ -107,41 +20,6 @@ environment variable and returns its path.
10720
file
10821
```
10922

110-
## `prefixLines` *prefix* *list*
111-
112-
`prefixLines` returns a string consisting of each item in *list*
113-
with the prefix *prefix*.
114-
115-
```text
116-
{{ list "this is" "a multi-line" "comment" | prefixLines "# " }}
117-
118-
# this is
119-
# a multi-line
120-
# comment
121-
```
122-
123-
## `quote` *input*
124-
125-
`quote` returns a double-quoted string literal containing *input*.
126-
*input* can be a string or list of strings.
127-
128-
```text
129-
{{ "foobar" | quote }}
130-
131-
"foobar"
132-
```
133-
134-
## `regexpReplaceAll` *pattern* *replacement* *string*
135-
136-
`regexpReplaceAll` replaces all instances of *pattern*
137-
with *replacement* in *string*.
138-
139-
```text
140-
{{ "foobar" | regexpReplaceAll "o*b" "" }}
141-
142-
far
143-
```
144-
14523
## `stat` *path*
14624

14725
`stat` returns a map representation of executing
@@ -152,51 +30,3 @@ far
15230
15331
file
15432
```
155-
156-
## `toJSON` *input*
157-
158-
`toJSON` returns a JSON string representation of *input*.
159-
160-
```text
161-
{{ list "foo" "bar" "baz" }}
162-
163-
["foo","bar","baz"]
164-
```
165-
166-
## `toLower` *string*
167-
168-
`toLower` returns *string* with all letters converted to lower case.
169-
170-
```text
171-
{{ toLower "FOOBAR" }}
172-
173-
foobar
174-
```
175-
176-
## `toString` *input*
177-
178-
`toString` returns the string representation of *input*.
179-
180-
```text
181-
{{ toString 10 }}
182-
```
183-
184-
## `toUpper` *string*
185-
186-
`toUpper` returns *string* with all letters converted to upper case.
187-
188-
```text
189-
{{ toUpper "foobar" }}
190-
191-
FOOBAR
192-
```
193-
194-
## `trimSpace` *string*
195-
196-
`trimSpace` returns *string* with all spaces removed.
197-
198-
```text
199-
{{ " foobar " | trimSpace }}
200-
201-
foobar
202-
```

‎internal/utils/utils.go

+196
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
package utils
2+
3+
import (
4+
"fmt"
5+
"io/fs"
6+
)
7+
8+
// fileModeTypeNames maps file mode types to human-readable strings.
9+
var fileModeTypeNames = map[fs.FileMode]string{
10+
0: "file",
11+
fs.ModeDir: "dir",
12+
fs.ModeSymlink: "symlink",
13+
fs.ModeNamedPipe: "named pipe",
14+
fs.ModeSocket: "socket",
15+
fs.ModeDevice: "device",
16+
fs.ModeCharDevice: "char device",
17+
}
18+
19+
// eachByteSlice transforms a function that takes a single `[]byte` and returns
20+
// a `T` to a function that takes zero or more `[]byte`-like arguments and
21+
// returns zero or more `T`s.
22+
func EachByteSlice[T any](f func([]byte) T) func(any) any {
23+
return func(arg any) any {
24+
switch arg := arg.(type) {
25+
case []byte:
26+
return f(arg)
27+
case [][]byte:
28+
result := make([]T, 0, len(arg))
29+
for _, a := range arg {
30+
result = append(result, f(a))
31+
}
32+
return result
33+
case string:
34+
return f([]byte(arg))
35+
case []string:
36+
result := make([]T, 0, len(arg))
37+
for _, a := range arg {
38+
result = append(result, f([]byte(a)))
39+
}
40+
return result
41+
default:
42+
panic(fmt.Sprintf("%T: unsupported argument type", arg))
43+
}
44+
}
45+
}
46+
47+
// eachByteSliceErr transforms a function that takes a single `[]byte` and
48+
// returns a `T` and an `error` into a function that takes zero or more
49+
// `[]byte`-like arguments and returns zero or more `Ts` and an error.
50+
func EachByteSliceErr[T any](f func([]byte) (T, error)) func(any) any {
51+
return func(arg any) any {
52+
switch arg := arg.(type) {
53+
case []byte:
54+
result, err := f(arg)
55+
if err != nil {
56+
panic(err)
57+
}
58+
return result
59+
case [][]byte:
60+
result := make([]T, 0, len(arg))
61+
for _, a := range arg {
62+
r, err := f(a)
63+
if err != nil {
64+
panic(err)
65+
}
66+
result = append(result, r)
67+
}
68+
return result
69+
case string:
70+
result, err := f([]byte(arg))
71+
if err != nil {
72+
panic(err)
73+
}
74+
return result
75+
case []string:
76+
result := make([]T, 0, len(arg))
77+
for _, a := range arg {
78+
r, err := f([]byte(a))
79+
if err != nil {
80+
panic(err)
81+
}
82+
result = append(result, r)
83+
}
84+
return result
85+
default:
86+
panic(fmt.Sprintf("%T: unsupported argument type", arg))
87+
}
88+
}
89+
}
90+
91+
// eachString transforms a function that takes a single `string`-like argument
92+
// and returns a `T` into a function that takes zero or more `string`-like
93+
// arguments and returns zero or more `T`s.
94+
func EachString[T any](f func(string) T) func(any) any {
95+
return func(arg any) any {
96+
switch arg := arg.(type) {
97+
case string:
98+
return f(arg)
99+
case []string:
100+
result := make([]T, 0, len(arg))
101+
for _, a := range arg {
102+
result = append(result, f(a))
103+
}
104+
return result
105+
case []byte:
106+
return f(string(arg))
107+
case [][]byte:
108+
result := make([]T, 0, len(arg))
109+
for _, a := range arg {
110+
result = append(result, f(string(a)))
111+
}
112+
return result
113+
case []any:
114+
result := make([]T, 0, len(arg))
115+
for _, a := range arg {
116+
switch a := a.(type) {
117+
case string:
118+
result = append(result, f(a))
119+
case []byte:
120+
result = append(result, f(string(a)))
121+
default:
122+
panic(fmt.Sprintf("%T: unsupported argument type", a))
123+
}
124+
}
125+
return result
126+
default:
127+
panic(fmt.Sprintf("%T: unsupported argument type", arg))
128+
}
129+
}
130+
}
131+
132+
// eachStringErr transforms a function that takes a single `string`-like argument
133+
// and returns a `T` and an `error` into a function that takes zero or more
134+
// `string`-like arguments and returns zero or more `T`s and an `error`.
135+
func EachStringErr[T any](f func(string) (T, error)) func(any) any {
136+
return func(arg any) any {
137+
switch arg := arg.(type) {
138+
case string:
139+
result, err := f(arg)
140+
if err != nil {
141+
panic(err)
142+
}
143+
return result
144+
case []string:
145+
result := make([]T, 0, len(arg))
146+
for _, a := range arg {
147+
r, err := f(a)
148+
if err != nil {
149+
panic(err)
150+
}
151+
result = append(result, r)
152+
}
153+
return result
154+
case []byte:
155+
result, err := f(string(arg))
156+
if err != nil {
157+
panic(err)
158+
}
159+
return result
160+
case [][]byte:
161+
result := make([]T, 0, len(arg))
162+
for _, a := range arg {
163+
r, err := f(string(a))
164+
if err != nil {
165+
panic(err)
166+
}
167+
result = append(result, r)
168+
}
169+
return result
170+
default:
171+
panic(fmt.Sprintf("%T: unsupported argument type", arg))
172+
}
173+
}
174+
}
175+
176+
// fileInfoToMap returns a `map[string]any` of `fileInfo`'s fields.
177+
func FileInfoToMap(fileInfo fs.FileInfo) map[string]any {
178+
return map[string]any{
179+
"name": fileInfo.Name(),
180+
"size": fileInfo.Size(),
181+
"mode": int(fileInfo.Mode()),
182+
"perm": int(fileInfo.Mode().Perm()),
183+
"modTime": fileInfo.ModTime().Unix(),
184+
"isDir": fileInfo.IsDir(),
185+
"type": fileModeTypeNames[fileInfo.Mode()&fs.ModeType],
186+
}
187+
}
188+
189+
// reverseArgs2 transforms a function that takes two arguments and returns an
190+
// `R` into a function that takes the arguments in reverse order and returns an
191+
// `R`.
192+
func ReverseArgs2[T1, T2, R any](f func(T1, T2) R) func(T2, T1) R {
193+
return func(arg1 T2, arg2 T1) R {
194+
return f(arg2, arg1)
195+
}
196+
}

‎pkg/booleanfuncs/booleanfuncs.go

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package booleanfuncs
2+
3+
import (
4+
"strings"
5+
"text/template"
6+
)
7+
8+
var FuncMap = template.FuncMap{
9+
"eqFold": eqFoldTemplateFunc,
10+
}
11+
12+
// eqFoldTemplateFunc is the core implementation of the `eqFold` template
13+
// function.
14+
func eqFoldTemplateFunc(first, second string, more ...string) bool {
15+
if strings.EqualFold(first, second) {
16+
return true
17+
}
18+
for _, s := range more {
19+
if strings.EqualFold(first, s) {
20+
return true
21+
}
22+
}
23+
return false
24+
}
+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package conversionfuncs
2+
3+
import (
4+
"encoding/hex"
5+
"encoding/json"
6+
"fmt"
7+
"strconv"
8+
"text/template"
9+
10+
"github.com/chezmoi/templatefuncs/internal/utils"
11+
)
12+
13+
var FuncMap = template.FuncMap{
14+
"fromJSON": utils.EachByteSliceErr(fromJSONTemplateFunc),
15+
"hexDecode": utils.EachStringErr(hex.DecodeString),
16+
"hexEncode": utils.EachByteSlice(hex.EncodeToString),
17+
"toJSON": toJSONTemplateFunc,
18+
"toString": toStringTemplateFunc,
19+
}
20+
21+
// fromJSONTemplateFunc is the core implementation of the `fromJSON` template
22+
// function.
23+
func fromJSONTemplateFunc(data []byte) (any, error) {
24+
var result any
25+
if err := json.Unmarshal(data, &result); err != nil {
26+
return nil, err
27+
}
28+
return result, nil
29+
}
30+
31+
// toJSONTemplateFunc is the core implementation of the `toJSON` template
32+
// function.
33+
func toJSONTemplateFunc(arg any) []byte {
34+
data, err := json.Marshal(arg)
35+
if err != nil {
36+
panic(err)
37+
}
38+
return data
39+
}
40+
41+
// toStringTemplateFunc is the core implementation of the `toString` template
42+
// function.
43+
func toStringTemplateFunc(arg any) string {
44+
// FIXME add more types
45+
switch arg := arg.(type) {
46+
case string:
47+
return arg
48+
case []byte:
49+
return string(arg)
50+
case bool:
51+
return strconv.FormatBool(arg)
52+
case float32:
53+
return strconv.FormatFloat(float64(arg), 'f', -1, 32)
54+
case float64:
55+
return strconv.FormatFloat(arg, 'f', -1, 64)
56+
case int:
57+
return strconv.Itoa(arg)
58+
case int32:
59+
return strconv.FormatInt(int64(arg), 10)
60+
case int64:
61+
return strconv.FormatInt(arg, 10)
62+
default:
63+
panic(fmt.Sprintf("%T: unsupported type", arg))
64+
}
65+
}

‎pkg/listfuncs/listfuncs.go

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package listfuncs
2+
3+
import (
4+
"strings"
5+
"text/template"
6+
7+
"github.com/chezmoi/templatefuncs/internal/utils"
8+
)
9+
10+
var FuncMap = template.FuncMap{
11+
"join": utils.ReverseArgs2(strings.Join),
12+
"list": listTemplateFunc,
13+
"prefixLines": prefixLinesTemplateFunc,
14+
}
15+
16+
// listTemplateFunc is the core implementation of the `list` template function.
17+
func listTemplateFunc(args ...any) []any {
18+
return args
19+
}
20+
21+
// prefixLinesTemplateFunc is the core implementation of the `prefixLines`
22+
// template function.
23+
func prefixLinesTemplateFunc(prefix, s string) string {
24+
type stateType int
25+
const (
26+
startOfLine stateType = iota
27+
inLine
28+
)
29+
30+
state := startOfLine
31+
var builder strings.Builder
32+
builder.Grow(2 * len(s))
33+
for _, r := range s {
34+
switch state {
35+
case startOfLine:
36+
if _, err := builder.WriteString(prefix); err != nil {
37+
panic(err)
38+
}
39+
if _, err := builder.WriteRune(r); err != nil {
40+
panic(err)
41+
}
42+
if r != '\n' {
43+
state = inLine
44+
}
45+
case inLine:
46+
if _, err := builder.WriteRune(r); err != nil {
47+
panic(err)
48+
}
49+
if r == '\n' {
50+
state = startOfLine
51+
}
52+
}
53+
}
54+
return builder.String()
55+
}

‎pkg/stringfuncs/stringfuncs.go

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package stringfuncs
2+
3+
import (
4+
"regexp"
5+
"strconv"
6+
"strings"
7+
"text/template"
8+
9+
"github.com/chezmoi/templatefuncs/internal/utils"
10+
)
11+
12+
var FuncMap = template.FuncMap{
13+
"contains": utils.ReverseArgs2(strings.Contains),
14+
"hasPrefix": utils.ReverseArgs2(strings.HasPrefix),
15+
"hasSuffix": utils.ReverseArgs2(strings.HasSuffix),
16+
"quote": utils.EachString(strconv.Quote),
17+
"regexpReplaceAll": regexpReplaceAllTemplateFunc,
18+
"toLower": utils.EachString(strings.ToLower),
19+
"toUpper": utils.EachString(strings.ToUpper),
20+
"trimSpace": utils.EachString(strings.TrimSpace),
21+
}
22+
23+
// regexpReplaceAllTemplateFunc is the core implementation of the
24+
// `regexpReplaceAll` template function.
25+
func regexpReplaceAllTemplateFunc(expr, repl, s string) string {
26+
return regexp.MustCompile(expr).ReplaceAllString(s, repl)
27+
}

‎templatefuncs.go

+20-324
Original file line numberDiff line numberDiff line change
@@ -1,120 +1,37 @@
11
package templatefuncs
22

33
import (
4-
"encoding/hex"
5-
"encoding/json"
64
"errors"
7-
"fmt"
85
"io/fs"
6+
"maps"
97
"os"
108
"os/exec"
11-
"regexp"
12-
"strconv"
13-
"strings"
149
"text/template"
15-
)
1610

17-
// fileModeTypeNames maps file mode types to human-readable strings.
18-
var fileModeTypeNames = map[fs.FileMode]string{
19-
0: "file",
20-
fs.ModeDir: "dir",
21-
fs.ModeSymlink: "symlink",
22-
fs.ModeNamedPipe: "named pipe",
23-
fs.ModeSocket: "socket",
24-
fs.ModeDevice: "device",
25-
fs.ModeCharDevice: "char device",
26-
}
11+
"github.com/chezmoi/templatefuncs/internal/utils"
12+
"github.com/chezmoi/templatefuncs/pkg/booleanfuncs"
13+
"github.com/chezmoi/templatefuncs/pkg/conversionfuncs"
14+
"github.com/chezmoi/templatefuncs/pkg/listfuncs"
15+
"github.com/chezmoi/templatefuncs/pkg/stringfuncs"
16+
)
2717

2818
// NewFuncMap returns a new [text/template.FuncMap] containing all template
2919
// functions.
3020
func NewFuncMap() template.FuncMap {
31-
return template.FuncMap{
32-
"contains": reverseArgs2(strings.Contains),
33-
"eqFold": eqFoldTemplateFunc,
34-
"fromJSON": eachByteSliceErr(fromJSONTemplateFunc),
35-
"hasPrefix": reverseArgs2(strings.HasPrefix),
36-
"hasSuffix": reverseArgs2(strings.HasSuffix),
37-
"hexDecode": eachStringErr(hex.DecodeString),
38-
"hexEncode": eachByteSlice(hex.EncodeToString),
39-
"join": reverseArgs2(strings.Join),
40-
"list": listTemplateFunc,
41-
"lookPath": eachStringErr(lookPathTemplateFunc),
42-
"lstat": eachString(lstatTemplateFunc),
43-
"prefixLines": prefixLinesTemplateFunc,
44-
"quote": eachString(strconv.Quote),
45-
"regexpReplaceAll": regexpReplaceAllTemplateFunc,
46-
"stat": eachString(statTemplateFunc),
47-
"toJSON": toJSONTemplateFunc,
48-
"toLower": eachString(strings.ToLower),
49-
"toString": toStringTemplateFunc,
50-
"toUpper": eachString(strings.ToUpper),
51-
"trimSpace": eachString(strings.TrimSpace),
52-
}
53-
}
21+
funcMap := template.FuncMap{}
5422

55-
// prefixLinesTemplateFunc is the core implementation of the `prefixLines`
56-
// template function.
57-
func prefixLinesTemplateFunc(prefix, s string) string {
58-
type stateType int
59-
const (
60-
startOfLine stateType = iota
61-
inLine
62-
)
23+
maps.Copy(funcMap, template.FuncMap{
24+
"lookPath": utils.EachStringErr(lookPathTemplateFunc),
25+
"lstat": utils.EachString(lstatTemplateFunc),
26+
"stat": utils.EachString(statTemplateFunc),
27+
})
6328

64-
state := startOfLine
65-
var builder strings.Builder
66-
builder.Grow(2 * len(s))
67-
for _, r := range s {
68-
switch state {
69-
case startOfLine:
70-
if _, err := builder.WriteString(prefix); err != nil {
71-
panic(err)
72-
}
73-
if _, err := builder.WriteRune(r); err != nil {
74-
panic(err)
75-
}
76-
if r != '\n' {
77-
state = inLine
78-
}
79-
case inLine:
80-
if _, err := builder.WriteRune(r); err != nil {
81-
panic(err)
82-
}
83-
if r == '\n' {
84-
state = startOfLine
85-
}
86-
}
87-
}
88-
return builder.String()
89-
}
29+
maps.Copy(funcMap, booleanfuncs.FuncMap)
30+
maps.Copy(funcMap, conversionfuncs.FuncMap)
31+
maps.Copy(funcMap, listfuncs.FuncMap)
32+
maps.Copy(funcMap, stringfuncs.FuncMap)
9033

91-
// eqFoldTemplateFunc is the core implementation of the `eqFold` template
92-
// function.
93-
func eqFoldTemplateFunc(first, second string, more ...string) bool {
94-
if strings.EqualFold(first, second) {
95-
return true
96-
}
97-
for _, s := range more {
98-
if strings.EqualFold(first, s) {
99-
return true
100-
}
101-
}
102-
return false
103-
}
104-
105-
// fromJSONTemplateFunc is the core implementation of the `fromJSON` template
106-
// function.
107-
func fromJSONTemplateFunc(data []byte) (any, error) {
108-
var result any
109-
if err := json.Unmarshal(data, &result); err != nil {
110-
return nil, err
111-
}
112-
return result, nil
113-
}
114-
115-
// listTemplateFunc is the core implementation of the `list` template function.
116-
func listTemplateFunc(args ...any) []any {
117-
return args
34+
return funcMap
11835
}
11936

12037
// lookPathTemplateFunc is the core implementation of the `lookPath` template
@@ -137,243 +54,22 @@ func lookPathTemplateFunc(file string) (string, error) {
13754
func lstatTemplateFunc(name string) any {
13855
switch fileInfo, err := os.Lstat(name); {
13956
case err == nil:
140-
return fileInfoToMap(fileInfo)
57+
return utils.FileInfoToMap(fileInfo)
14158
case errors.Is(err, fs.ErrNotExist):
14259
return nil
14360
default:
14461
panic(err)
14562
}
14663
}
14764

148-
// regexpReplaceAllTemplateFunc is the core implementation of the
149-
// `regexpReplaceAll` template function.
150-
func regexpReplaceAllTemplateFunc(expr, repl, s string) string {
151-
return regexp.MustCompile(expr).ReplaceAllString(s, repl)
152-
}
153-
15465
// statTemplateFunc is the core implementation of the `stat` template function.
15566
func statTemplateFunc(name string) any {
15667
switch fileInfo, err := os.Stat(name); {
15768
case err == nil:
158-
return fileInfoToMap(fileInfo)
69+
return utils.FileInfoToMap(fileInfo)
15970
case errors.Is(err, fs.ErrNotExist):
16071
return nil
16172
default:
16273
panic(err)
16374
}
16475
}
165-
166-
// toJSONTemplateFunc is the core implementation of the `toJSON` template
167-
// function.
168-
func toJSONTemplateFunc(arg any) []byte {
169-
data, err := json.Marshal(arg)
170-
if err != nil {
171-
panic(err)
172-
}
173-
return data
174-
}
175-
176-
// toStringTemplateFunc is the core implementation of the `toString` template
177-
// function.
178-
func toStringTemplateFunc(arg any) string {
179-
// FIXME add more types
180-
switch arg := arg.(type) {
181-
case string:
182-
return arg
183-
case []byte:
184-
return string(arg)
185-
case bool:
186-
return strconv.FormatBool(arg)
187-
case float32:
188-
return strconv.FormatFloat(float64(arg), 'f', -1, 32)
189-
case float64:
190-
return strconv.FormatFloat(arg, 'f', -1, 64)
191-
case int:
192-
return strconv.Itoa(arg)
193-
case int32:
194-
return strconv.FormatInt(int64(arg), 10)
195-
case int64:
196-
return strconv.FormatInt(arg, 10)
197-
default:
198-
panic(fmt.Sprintf("%T: unsupported type", arg))
199-
}
200-
}
201-
202-
// eachByteSlice transforms a function that takes a single `[]byte` and returns
203-
// a `T` to a function that takes zero or more `[]byte`-like arguments and
204-
// returns zero or more `T`s.
205-
func eachByteSlice[T any](f func([]byte) T) func(any) any {
206-
return func(arg any) any {
207-
switch arg := arg.(type) {
208-
case []byte:
209-
return f(arg)
210-
case [][]byte:
211-
result := make([]T, 0, len(arg))
212-
for _, a := range arg {
213-
result = append(result, f(a))
214-
}
215-
return result
216-
case string:
217-
return f([]byte(arg))
218-
case []string:
219-
result := make([]T, 0, len(arg))
220-
for _, a := range arg {
221-
result = append(result, f([]byte(a)))
222-
}
223-
return result
224-
default:
225-
panic(fmt.Sprintf("%T: unsupported argument type", arg))
226-
}
227-
}
228-
}
229-
230-
// eachByteSliceErr transforms a function that takes a single `[]byte` and
231-
// returns a `T` and an `error` into a function that takes zero or more
232-
// `[]byte`-like arguments and returns zero or more `Ts` and an error.
233-
func eachByteSliceErr[T any](f func([]byte) (T, error)) func(any) any {
234-
return func(arg any) any {
235-
switch arg := arg.(type) {
236-
case []byte:
237-
result, err := f(arg)
238-
if err != nil {
239-
panic(err)
240-
}
241-
return result
242-
case [][]byte:
243-
result := make([]T, 0, len(arg))
244-
for _, a := range arg {
245-
r, err := f(a)
246-
if err != nil {
247-
panic(err)
248-
}
249-
result = append(result, r)
250-
}
251-
return result
252-
case string:
253-
result, err := f([]byte(arg))
254-
if err != nil {
255-
panic(err)
256-
}
257-
return result
258-
case []string:
259-
result := make([]T, 0, len(arg))
260-
for _, a := range arg {
261-
r, err := f([]byte(a))
262-
if err != nil {
263-
panic(err)
264-
}
265-
result = append(result, r)
266-
}
267-
return result
268-
default:
269-
panic(fmt.Sprintf("%T: unsupported argument type", arg))
270-
}
271-
}
272-
}
273-
274-
// eachString transforms a function that takes a single `string`-like argument
275-
// and returns a `T` into a function that takes zero or more `string`-like
276-
// arguments and returns zero or more `T`s.
277-
func eachString[T any](f func(string) T) func(any) any {
278-
return func(arg any) any {
279-
switch arg := arg.(type) {
280-
case string:
281-
return f(arg)
282-
case []string:
283-
result := make([]T, 0, len(arg))
284-
for _, a := range arg {
285-
result = append(result, f(a))
286-
}
287-
return result
288-
case []byte:
289-
return f(string(arg))
290-
case [][]byte:
291-
result := make([]T, 0, len(arg))
292-
for _, a := range arg {
293-
result = append(result, f(string(a)))
294-
}
295-
return result
296-
case []any:
297-
result := make([]T, 0, len(arg))
298-
for _, a := range arg {
299-
switch a := a.(type) {
300-
case string:
301-
result = append(result, f(a))
302-
case []byte:
303-
result = append(result, f(string(a)))
304-
default:
305-
panic(fmt.Sprintf("%T: unsupported argument type", a))
306-
}
307-
}
308-
return result
309-
default:
310-
panic(fmt.Sprintf("%T: unsupported argument type", arg))
311-
}
312-
}
313-
}
314-
315-
// eachStringErr transforms a function that takes a single `string`-like argument
316-
// and returns a `T` and an `error` into a function that takes zero or more
317-
// `string`-like arguments and returns zero or more `T`s and an `error`.
318-
func eachStringErr[T any](f func(string) (T, error)) func(any) any {
319-
return func(arg any) any {
320-
switch arg := arg.(type) {
321-
case string:
322-
result, err := f(arg)
323-
if err != nil {
324-
panic(err)
325-
}
326-
return result
327-
case []string:
328-
result := make([]T, 0, len(arg))
329-
for _, a := range arg {
330-
r, err := f(a)
331-
if err != nil {
332-
panic(err)
333-
}
334-
result = append(result, r)
335-
}
336-
return result
337-
case []byte:
338-
result, err := f(string(arg))
339-
if err != nil {
340-
panic(err)
341-
}
342-
return result
343-
case [][]byte:
344-
result := make([]T, 0, len(arg))
345-
for _, a := range arg {
346-
r, err := f(string(a))
347-
if err != nil {
348-
panic(err)
349-
}
350-
result = append(result, r)
351-
}
352-
return result
353-
default:
354-
panic(fmt.Sprintf("%T: unsupported argument type", arg))
355-
}
356-
}
357-
}
358-
359-
// fileInfoToMap returns a `map[string]any` of `fileInfo`'s fields.
360-
func fileInfoToMap(fileInfo fs.FileInfo) map[string]any {
361-
return map[string]any{
362-
"name": fileInfo.Name(),
363-
"size": fileInfo.Size(),
364-
"mode": int(fileInfo.Mode()),
365-
"perm": int(fileInfo.Mode().Perm()),
366-
"modTime": fileInfo.ModTime().Unix(),
367-
"isDir": fileInfo.IsDir(),
368-
"type": fileModeTypeNames[fileInfo.Mode()&fs.ModeType],
369-
}
370-
}
371-
372-
// reverseArgs2 transforms a function that takes two arguments and returns an
373-
// `R` into a function that takes the arguments in reverse order and returns an
374-
// `R`.
375-
func reverseArgs2[T1, T2, R any](f func(T1, T2) R) func(T2, T1) R {
376-
return func(arg1 T2, arg2 T1) R {
377-
return f(arg2, arg1)
378-
}
379-
}

‎templatefuncs_test.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"text/template"
88

99
"github.com/alecthomas/assert/v2"
10+
11+
"github.com/chezmoi/templatefuncs/internal/utils"
1012
)
1113

1214
func TestEachString(t *testing.T) {
@@ -37,7 +39,7 @@ func TestEachString(t *testing.T) {
3739
},
3840
} {
3941
t.Run(strconv.Itoa(i), func(t *testing.T) {
40-
f := eachString(tc.f)
42+
f := utils.EachString(tc.f)
4143
assert.Equal(t, tc.expected, f(tc.arg))
4244
})
4345
}

0 commit comments

Comments
 (0)
Please sign in to comment.