From 35cf4b641e333acbbd89c236b20a4aabb769229f Mon Sep 17 00:00:00 2001 From: Braden Hilton Date: Tue, 14 May 2024 22:26:31 +0100 Subject: [PATCH 1/2] chore: split and organize files --- .github/workflows/main.yml | 2 +- .golangci.yml | 5 +- docs/booleanfuncs.md | 12 + docs/conversionfuncs.md | 47 ++++ docs/docs.go | 67 ++++- docs/docs_test.go | 17 +- docs/listfuncs.md | 32 +++ docs/stringfuncs.md | 83 ++++++ docs/templatefuncs.md | 170 ------------ internal/utils/utils.go | 196 ++++++++++++++ pkg/booleanfuncs/booleanfuncs.go | 24 ++ pkg/conversionfuncs/conversionfuncs.go | 65 +++++ pkg/listfuncs/listfuncs.go | 55 ++++ pkg/stringfuncs/stringfuncs.go | 27 ++ templatefuncs.go | 344 ++----------------------- templatefuncs_test.go | 4 +- 16 files changed, 633 insertions(+), 517 deletions(-) create mode 100644 docs/booleanfuncs.md create mode 100644 docs/conversionfuncs.md create mode 100644 docs/listfuncs.md create mode 100644 docs/stringfuncs.md create mode 100644 internal/utils/utils.go create mode 100644 pkg/booleanfuncs/booleanfuncs.go create mode 100644 pkg/conversionfuncs/conversionfuncs.go create mode 100644 pkg/listfuncs/listfuncs.go create mode 100644 pkg/stringfuncs/stringfuncs.go diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c4e248d..fb62814 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -20,7 +20,7 @@ jobs: - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 - uses: golangci/golangci-lint-action@9d1e0624a798bb64f6c3cea93db47765312263dc with: - version: v1.55.2 + version: v1.58.1 - uses: DavidAnson/markdownlint-cli2-action@b4c9feab76d8025d1e83c653fa3990936df0e6c8 with: globs: '**/*.md' diff --git a/.golangci.yml b/.golangci.yml index 8783805..5e62d94 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -11,11 +11,11 @@ linters: - dupl - dupword - durationcheck + - err113 - errcheck - errchkjson - errname - errorlint - - execinquery - exhaustive - exportloopref - forbidigo @@ -30,7 +30,6 @@ linters: - gocritic - gocyclo - godot - - goerr113 - gofmt - gofumpt - goheader @@ -136,7 +135,7 @@ issues: - gosec path: "internal/" - linters: - - goerr113 + - err113 text: do not define dynamic errors, use wrapped static errors instead - linters: - gochecknoinits diff --git a/docs/booleanfuncs.md b/docs/booleanfuncs.md new file mode 100644 index 0000000..102e9a9 --- /dev/null +++ b/docs/booleanfuncs.md @@ -0,0 +1,12 @@ +# Boolean Functions + +## `eqFold` *string1* *string2* [*extraStrings*...] + +`eqFold` returns the boolean truth of comparing *string1* with *string2* +and any number of *extraStrings* under Unicode case-folding. + +```text +{{ eqFold "föö" "FOO" }} + +true +``` diff --git a/docs/conversionfuncs.md b/docs/conversionfuncs.md new file mode 100644 index 0000000..0e2bb02 --- /dev/null +++ b/docs/conversionfuncs.md @@ -0,0 +1,47 @@ +# Conversion Functions + +## `fromJSON` *jsontext* + +`fromJSON` parses *jsontext* as JSON and returns the parsed value. + +```text +{{ `{ "foo": "bar" }` | fromJSON }} +``` + +## `hexDecode` *hextext* + +`hexDecode` returns the bytes represented by *hextext*. + +```text +{{ hexDecode "666f6f626172" }} + +foobar +``` + +## `hexEncode` *string* + +`hexEncode` returns the hexadecimal encoding of *string*. + +```text +{{ hexEncode "foobar" }} + +666f6f626172 +``` + +## `toJSON` *input* + +`toJSON` returns a JSON string representation of *input*. + +```text +{{ list "foo" "bar" "baz" }} + +["foo","bar","baz"] +``` + +## `toString` *input* + +`toString` returns the string representation of *input*. + +```text +{{ toString 10 }} +``` diff --git a/docs/docs.go b/docs/docs.go index 5bc018b..738424a 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1,44 +1,75 @@ package docs import ( - _ "embed" + "embed" + "log" + "maps" "regexp" "strings" ) -//go:embed templatefuncs.md -var templateFuncsStr string - type Reference struct { + Type string Title string Body string Example string } -var References map[string]Reference +//go:embed *.md +var f embed.FS -func init() { - newlineRx := regexp.MustCompile(`\r?\n`) +var ( + References map[string]Reference + newlineRx = regexp.MustCompile(`\r?\n`) + pageTitleRx = regexp.MustCompile(`^#\s+(\S+)`) // Template function names must start with a letter or underscore // and can subsequently contain letters, underscores and digits. - funcNameRx := regexp.MustCompile("`" + `([a-zA-Z_]\w*)` + "`") + funcNameRx = regexp.MustCompile("`" + `([a-zA-Z_]\w*)` + "`") +) - References = make(map[string]Reference) +func readFiles() []string { + fileContents := []string{} + + fileInfos, err := f.ReadDir(".") + if err != nil { + log.Fatal(err) + } + + for _, fileInfo := range fileInfos { + if fileInfo.IsDir() || !strings.HasSuffix(fileInfo.Name(), ".md") { + continue + } + content, err := f.ReadFile(fileInfo.Name()) + if err != nil { + log.Fatal(err) + } + fileContents = append(fileContents, string(content)) + } + + return fileContents +} + +func parseFile(file string) map[string]Reference { + references := make(map[string]Reference) var reference Reference var funcName string var b strings.Builder var e strings.Builder inExample := false - for _, line := range newlineRx.Split(templateFuncsStr, -1) { + lines := newlineRx.Split(file, -1) + funcType, lines := pageTitleRx.FindStringSubmatch(lines[0])[1], lines[1:] + + for _, line := range lines { switch { case strings.HasPrefix(line, "## "): if reference.Title != "" { - References[funcName] = reference + references[funcName] = reference } funcName = funcNameRx.FindStringSubmatch(line)[1] reference = Reference{} + reference.Type = funcType reference.Title = strings.TrimPrefix(line, "## ") case strings.HasPrefix(line, "```"): if !inExample { @@ -63,6 +94,18 @@ func init() { } if reference.Title != "" { - References[funcName] = reference + references[funcName] = reference + } + + return references +} + +func init() { + References = make(map[string]Reference) + + files := readFiles() + + for _, file := range files { + maps.Copy(References, parseFile(file)) } } diff --git a/docs/docs_test.go b/docs/docs_test.go index 70cdb00..59d7536 100644 --- a/docs/docs_test.go +++ b/docs/docs_test.go @@ -10,6 +10,7 @@ import ( func TestReferences(t *testing.T) { assert.Equal(t, docs.Reference{ + Type: "String", Title: "`contains` *substring* *string*", Body: "`contains` returns whether *substring* is in *string*.", Example: "" + @@ -20,12 +21,16 @@ func TestReferences(t *testing.T) { "```", }, docs.References["contains"]) assert.Equal(t, docs.Reference{ - Title: "`trimSpace` *string*", - Body: "`trimSpace` returns *string* with all spaces removed.", - Example: "```text\n" + - "{{ \" foobar \" | trimSpace }}\n" + + Type: "Boolean", + Title: "`eqFold` *string1* *string2* [*extraStrings*...]", + Body: "" + + "`eqFold` returns the boolean truth of comparing *string1* with *string2*\n" + + "and any number of *extraStrings* under Unicode case-folding.", + Example: "" + + "```text\n" + + "{{ eqFold \"föö\" \"FOO\" }}\n" + "\n" + - "foobar\n" + + "true\n" + "```", - }, docs.References["trimSpace"]) + }, docs.References["eqFold"]) } diff --git a/docs/listfuncs.md b/docs/listfuncs.md new file mode 100644 index 0000000..57d44d6 --- /dev/null +++ b/docs/listfuncs.md @@ -0,0 +1,32 @@ +# List Functions + +## `join` *delimiter* *list* + +`join` returns a string containing each item in *list* joined with *delimiter*. + +```text +{{ list "foo" "bar" "baz" | join "," }} + +foo,bar,baz +``` + +## `list` *items*... + +`list` creates a new list containing *items*. + +```text +{{ list "foo" "bar" "baz" }} +``` + +## `prefixLines` *prefix* *list* + +`prefixLines` returns a string consisting of each item in *list* +with the prefix *prefix*. + +```text +{{ list "this is" "a multi-line" "comment" | prefixLines "# " }} + +# this is +# a multi-line +# comment +``` diff --git a/docs/stringfuncs.md b/docs/stringfuncs.md new file mode 100644 index 0000000..ced34ff --- /dev/null +++ b/docs/stringfuncs.md @@ -0,0 +1,83 @@ +# String Functions + +## `contains` *substring* *string* + +`contains` returns whether *substring* is in *string*. + +```text +{{ "abc" | contains "ab" }} + +true +``` + +## `hasPrefix` *prefix* *string* + +`hasPrefix` returns whether *string* begins with *prefix*. + +```text +{{ "foobar" | hasPrefix "foo" }} + +true +``` + +## `hasSuffix` *suffix* *string* + +`hasSuffix` returns whether *string* ends with *suffix*. + +```text +{{ "foobar" | hasSuffix "bar" }} + +true +``` + +## `quote` *input* + +`quote` returns a double-quoted string literal containing *input*. +*input* can be a string or list of strings. + +```text +{{ "foobar" | quote }} + +"foobar" +``` + +## `regexpReplaceAll` *pattern* *replacement* *string* + +`regexpReplaceAll` replaces all instances of *pattern* +with *replacement* in *string*. + +```text +{{ "foobar" | regexpReplaceAll "o*b" "" }} + +far +``` + +## `toLower` *string* + +`toLower` returns *string* with all letters converted to lower case. + +```text +{{ toLower "FOOBAR" }} + +foobar +``` + +## `toUpper` *string* + +`toUpper` returns *string* with all letters converted to upper case. + +```text +{{ toUpper "foobar" }} + +FOOBAR +``` + +## `trimSpace` *string* + +`trimSpace` returns *string* with all spaces removed. + +```text +{{ " foobar " | trimSpace }} + +foobar +``` diff --git a/docs/templatefuncs.md b/docs/templatefuncs.md index 0a41b79..d4a82a2 100644 --- a/docs/templatefuncs.md +++ b/docs/templatefuncs.md @@ -1,92 +1,5 @@ # Template Functions -## `contains` *substring* *string* - -`contains` returns whether *substring* is in *string*. - -```text -{{ "abc" | contains "ab" }} - -true -``` - -## `eqFold` *string1* *string2* [*extraStrings*...] - -`eqFold` returns the boolean truth of comparing *string1* with *string2* -and any number of *extraStrings* under Unicode case-folding. - -```text -{{ eqFold "föö" "FOO" }} - -true -``` - -## `fromJSON` *jsontext* - -`fromJSON` parses *jsontext* as JSON and returns the parsed value. - -```text -{{ `{ "foo": "bar" }` | fromJSON }} -``` - -## `hasPrefix` *prefix* *string* - -`hasPrefix` returns whether *string* begins with *prefix*. - -```text -{{ "foobar" | hasPrefix "foo" }} - -true -``` - -## `hasSuffix` *suffix* *string* - -`hasSuffix` returns whether *string* ends with *suffix*. - -```text -{{ "foobar" | hasSuffix "bar" }} - -true -``` - -## `hexDecode` *hextext* - -`hexDecode` returns the bytes represented by *hextext*. - -```text -{{ hexDecode "666f6f626172" }} - -foobar -``` - -## `hexEncode` *string* - -`hexEncode` returns the hexadecimal encoding of *string*. - -```text -{{ hexEncode "foobar" }} - -666f6f626172 -``` - -## `join` *delimiter* *list* - -`join` returns a string containing each item in *list* joined with *delimiter*. - -```text -{{ list "foo" "bar" "baz" | join "," }} - -foo,bar,baz -``` - -## `list` *items*... - -`list` creates a new list containing *items*. - -```text -{{ list "foo" "bar" "baz" }} -``` - ## `lookPath` *file* `lookPath` searches for the executable *file* in the users `PATH` @@ -107,41 +20,6 @@ environment variable and returns its path. file ``` -## `prefixLines` *prefix* *list* - -`prefixLines` returns a string consisting of each item in *list* -with the prefix *prefix*. - -```text -{{ list "this is" "a multi-line" "comment" | prefixLines "# " }} - -# this is -# a multi-line -# comment -``` - -## `quote` *input* - -`quote` returns a double-quoted string literal containing *input*. -*input* can be a string or list of strings. - -```text -{{ "foobar" | quote }} - -"foobar" -``` - -## `regexpReplaceAll` *pattern* *replacement* *string* - -`regexpReplaceAll` replaces all instances of *pattern* -with *replacement* in *string*. - -```text -{{ "foobar" | regexpReplaceAll "o*b" "" }} - -far -``` - ## `stat` *path* `stat` returns a map representation of executing @@ -152,51 +30,3 @@ far file ``` - -## `toJSON` *input* - -`toJSON` returns a JSON string representation of *input*. - -```text -{{ list "foo" "bar" "baz" }} - -["foo","bar","baz"] -``` - -## `toLower` *string* - -`toLower` returns *string* with all letters converted to lower case. - -```text -{{ toLower "FOOBAR" }} - -foobar -``` - -## `toString` *input* - -`toString` returns the string representation of *input*. - -```text -{{ toString 10 }} -``` - -## `toUpper` *string* - -`toUpper` returns *string* with all letters converted to upper case. - -```text -{{ toUpper "foobar" }} - -FOOBAR -``` - -## `trimSpace` *string* - -`trimSpace` returns *string* with all spaces removed. - -```text -{{ " foobar " | trimSpace }} - -foobar -``` diff --git a/internal/utils/utils.go b/internal/utils/utils.go new file mode 100644 index 0000000..3f4ac84 --- /dev/null +++ b/internal/utils/utils.go @@ -0,0 +1,196 @@ +package utils + +import ( + "fmt" + "io/fs" +) + +// fileModeTypeNames maps file mode types to human-readable strings. +var fileModeTypeNames = map[fs.FileMode]string{ + 0: "file", + fs.ModeDir: "dir", + fs.ModeSymlink: "symlink", + fs.ModeNamedPipe: "named pipe", + fs.ModeSocket: "socket", + fs.ModeDevice: "device", + fs.ModeCharDevice: "char device", +} + +// EachByteSlice transforms a function that takes a single `[]byte` and returns +// a `T` to a function that takes zero or more `[]byte`-like arguments and +// returns zero or more `T`s. +func EachByteSlice[T any](f func([]byte) T) func(any) any { + return func(arg any) any { + switch arg := arg.(type) { + case []byte: + return f(arg) + case [][]byte: + result := make([]T, 0, len(arg)) + for _, a := range arg { + result = append(result, f(a)) + } + return result + case string: + return f([]byte(arg)) + case []string: + result := make([]T, 0, len(arg)) + for _, a := range arg { + result = append(result, f([]byte(a))) + } + return result + default: + panic(fmt.Sprintf("%T: unsupported argument type", arg)) + } + } +} + +// EachByteSliceErr transforms a function that takes a single `[]byte` and +// returns a `T` and an `error` into a function that takes zero or more +// `[]byte`-like arguments and returns zero or more `Ts` and an error. +func EachByteSliceErr[T any](f func([]byte) (T, error)) func(any) any { + return func(arg any) any { + switch arg := arg.(type) { + case []byte: + result, err := f(arg) + if err != nil { + panic(err) + } + return result + case [][]byte: + result := make([]T, 0, len(arg)) + for _, a := range arg { + r, err := f(a) + if err != nil { + panic(err) + } + result = append(result, r) + } + return result + case string: + result, err := f([]byte(arg)) + if err != nil { + panic(err) + } + return result + case []string: + result := make([]T, 0, len(arg)) + for _, a := range arg { + r, err := f([]byte(a)) + if err != nil { + panic(err) + } + result = append(result, r) + } + return result + default: + panic(fmt.Sprintf("%T: unsupported argument type", arg)) + } + } +} + +// EachString transforms a function that takes a single `string`-like argument +// and returns a `T` into a function that takes zero or more `string`-like +// arguments and returns zero or more `T`s. +func EachString[T any](f func(string) T) func(any) any { + return func(arg any) any { + switch arg := arg.(type) { + case string: + return f(arg) + case []string: + result := make([]T, 0, len(arg)) + for _, a := range arg { + result = append(result, f(a)) + } + return result + case []byte: + return f(string(arg)) + case [][]byte: + result := make([]T, 0, len(arg)) + for _, a := range arg { + result = append(result, f(string(a))) + } + return result + case []any: + result := make([]T, 0, len(arg)) + for _, a := range arg { + switch a := a.(type) { + case string: + result = append(result, f(a)) + case []byte: + result = append(result, f(string(a))) + default: + panic(fmt.Sprintf("%T: unsupported argument type", a)) + } + } + return result + default: + panic(fmt.Sprintf("%T: unsupported argument type", arg)) + } + } +} + +// EachStringErr transforms a function that takes a single `string`-like argument +// and returns a `T` and an `error` into a function that takes zero or more +// `string`-like arguments and returns zero or more `T`s and an `error`. +func EachStringErr[T any](f func(string) (T, error)) func(any) any { + return func(arg any) any { + switch arg := arg.(type) { + case string: + result, err := f(arg) + if err != nil { + panic(err) + } + return result + case []string: + result := make([]T, 0, len(arg)) + for _, a := range arg { + r, err := f(a) + if err != nil { + panic(err) + } + result = append(result, r) + } + return result + case []byte: + result, err := f(string(arg)) + if err != nil { + panic(err) + } + return result + case [][]byte: + result := make([]T, 0, len(arg)) + for _, a := range arg { + r, err := f(string(a)) + if err != nil { + panic(err) + } + result = append(result, r) + } + return result + default: + panic(fmt.Sprintf("%T: unsupported argument type", arg)) + } + } +} + +// FileInfoToMap returns a `map[string]any` of `fileInfo`'s fields. +func FileInfoToMap(fileInfo fs.FileInfo) map[string]any { + return map[string]any{ + "name": fileInfo.Name(), + "size": fileInfo.Size(), + "mode": int(fileInfo.Mode()), + "perm": int(fileInfo.Mode().Perm()), + "modTime": fileInfo.ModTime().Unix(), + "isDir": fileInfo.IsDir(), + "type": fileModeTypeNames[fileInfo.Mode()&fs.ModeType], + } +} + +// ReverseArgs2 transforms a function that takes two arguments and returns an +// `R` into a function that takes the arguments in reverse order and returns an +// `R`. +func ReverseArgs2[T1, T2, R any](f func(T1, T2) R) func(T2, T1) R { + return func(arg1 T2, arg2 T1) R { + return f(arg2, arg1) + } +} diff --git a/pkg/booleanfuncs/booleanfuncs.go b/pkg/booleanfuncs/booleanfuncs.go new file mode 100644 index 0000000..c32069b --- /dev/null +++ b/pkg/booleanfuncs/booleanfuncs.go @@ -0,0 +1,24 @@ +package booleanfuncs + +import ( + "strings" + "text/template" +) + +var FuncMap = template.FuncMap{ + "eqFold": eqFoldTemplateFunc, +} + +// eqFoldTemplateFunc is the core implementation of the `eqFold` template +// function. +func eqFoldTemplateFunc(first, second string, more ...string) bool { + if strings.EqualFold(first, second) { + return true + } + for _, s := range more { + if strings.EqualFold(first, s) { + return true + } + } + return false +} diff --git a/pkg/conversionfuncs/conversionfuncs.go b/pkg/conversionfuncs/conversionfuncs.go new file mode 100644 index 0000000..80a8724 --- /dev/null +++ b/pkg/conversionfuncs/conversionfuncs.go @@ -0,0 +1,65 @@ +package conversionfuncs + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "strconv" + "text/template" + + "github.com/chezmoi/templatefuncs/internal/utils" +) + +var FuncMap = template.FuncMap{ + "fromJSON": utils.EachByteSliceErr(fromJSONTemplateFunc), + "hexDecode": utils.EachStringErr(hex.DecodeString), + "hexEncode": utils.EachByteSlice(hex.EncodeToString), + "toJSON": toJSONTemplateFunc, + "toString": toStringTemplateFunc, +} + +// fromJSONTemplateFunc is the core implementation of the `fromJSON` template +// function. +func fromJSONTemplateFunc(data []byte) (any, error) { + var result any + if err := json.Unmarshal(data, &result); err != nil { + return nil, err + } + return result, nil +} + +// toJSONTemplateFunc is the core implementation of the `toJSON` template +// function. +func toJSONTemplateFunc(arg any) []byte { + data, err := json.Marshal(arg) + if err != nil { + panic(err) + } + return data +} + +// toStringTemplateFunc is the core implementation of the `toString` template +// function. +func toStringTemplateFunc(arg any) string { + // FIXME add more types + switch arg := arg.(type) { + case string: + return arg + case []byte: + return string(arg) + case bool: + return strconv.FormatBool(arg) + case float32: + return strconv.FormatFloat(float64(arg), 'f', -1, 32) + case float64: + return strconv.FormatFloat(arg, 'f', -1, 64) + case int: + return strconv.Itoa(arg) + case int32: + return strconv.FormatInt(int64(arg), 10) + case int64: + return strconv.FormatInt(arg, 10) + default: + panic(fmt.Sprintf("%T: unsupported type", arg)) + } +} diff --git a/pkg/listfuncs/listfuncs.go b/pkg/listfuncs/listfuncs.go new file mode 100644 index 0000000..add00a1 --- /dev/null +++ b/pkg/listfuncs/listfuncs.go @@ -0,0 +1,55 @@ +package listfuncs + +import ( + "strings" + "text/template" + + "github.com/chezmoi/templatefuncs/internal/utils" +) + +var FuncMap = template.FuncMap{ + "join": utils.ReverseArgs2(strings.Join), + "list": listTemplateFunc, + "prefixLines": prefixLinesTemplateFunc, +} + +// listTemplateFunc is the core implementation of the `list` template function. +func listTemplateFunc(args ...any) []any { + return args +} + +// prefixLinesTemplateFunc is the core implementation of the `prefixLines` +// template function. +func prefixLinesTemplateFunc(prefix, s string) string { + type stateType int + const ( + startOfLine stateType = iota + inLine + ) + + state := startOfLine + var builder strings.Builder + builder.Grow(2 * len(s)) + for _, r := range s { + switch state { + case startOfLine: + if _, err := builder.WriteString(prefix); err != nil { + panic(err) + } + if _, err := builder.WriteRune(r); err != nil { + panic(err) + } + if r != '\n' { + state = inLine + } + case inLine: + if _, err := builder.WriteRune(r); err != nil { + panic(err) + } + if r == '\n' { + state = startOfLine + } + } + } + return builder.String() +} diff --git a/pkg/stringfuncs/stringfuncs.go b/pkg/stringfuncs/stringfuncs.go new file mode 100644 index 0000000..80cb083 --- /dev/null +++ b/pkg/stringfuncs/stringfuncs.go @@ -0,0 +1,27 @@ +package stringfuncs + +import ( + "regexp" + "strconv" + "strings" + "text/template" + + "github.com/chezmoi/templatefuncs/internal/utils" +) + +var FuncMap = template.FuncMap{ + "contains": utils.ReverseArgs2(strings.Contains), + "hasPrefix": utils.ReverseArgs2(strings.HasPrefix), + "hasSuffix": utils.ReverseArgs2(strings.HasSuffix), + "quote": utils.EachString(strconv.Quote), + "regexpReplaceAll": regexpReplaceAllTemplateFunc, + "toLower": utils.EachString(strings.ToLower), + "toUpper": utils.EachString(strings.ToUpper), + "trimSpace": utils.EachString(strings.TrimSpace), +} + +// regexpReplaceAllTemplateFunc is the core implementation of the +// `regexpReplaceAll` template function. +func regexpReplaceAllTemplateFunc(expr, repl, s string) string { + return regexp.MustCompile(expr).ReplaceAllString(s, repl) +} diff --git a/templatefuncs.go b/templatefuncs.go index d44298e..a5f3051 100644 --- a/templatefuncs.go +++ b/templatefuncs.go @@ -1,120 +1,37 @@ package templatefuncs import ( - "encoding/hex" - "encoding/json" "errors" - "fmt" "io/fs" + "maps" "os" "os/exec" - "regexp" - "strconv" - "strings" "text/template" -) -// fileModeTypeNames maps file mode types to human-readable strings. -var fileModeTypeNames = map[fs.FileMode]string{ - 0: "file", - fs.ModeDir: "dir", - fs.ModeSymlink: "symlink", - fs.ModeNamedPipe: "named pipe", - fs.ModeSocket: "socket", - fs.ModeDevice: "device", - fs.ModeCharDevice: "char device", -} + "github.com/chezmoi/templatefuncs/internal/utils" + "github.com/chezmoi/templatefuncs/pkg/booleanfuncs" + "github.com/chezmoi/templatefuncs/pkg/conversionfuncs" + "github.com/chezmoi/templatefuncs/pkg/listfuncs" + "github.com/chezmoi/templatefuncs/pkg/stringfuncs" +) // NewFuncMap returns a new [text/template.FuncMap] containing all template // functions. func NewFuncMap() template.FuncMap { - return template.FuncMap{ - "contains": reverseArgs2(strings.Contains), - "eqFold": eqFoldTemplateFunc, - "fromJSON": eachByteSliceErr(fromJSONTemplateFunc), - "hasPrefix": reverseArgs2(strings.HasPrefix), - "hasSuffix": reverseArgs2(strings.HasSuffix), - "hexDecode": eachStringErr(hex.DecodeString), - "hexEncode": eachByteSlice(hex.EncodeToString), - "join": reverseArgs2(strings.Join), - "list": listTemplateFunc, - "lookPath": eachStringErr(lookPathTemplateFunc), - "lstat": eachString(lstatTemplateFunc), - "prefixLines": prefixLinesTemplateFunc, - "quote": eachString(strconv.Quote), - "regexpReplaceAll": regexpReplaceAllTemplateFunc, - "stat": eachString(statTemplateFunc), - "toJSON": toJSONTemplateFunc, - "toLower": eachString(strings.ToLower), - "toString": toStringTemplateFunc, - "toUpper": eachString(strings.ToUpper), - "trimSpace": eachString(strings.TrimSpace), - } -} + funcMap := template.FuncMap{} -// prefixLinesTemplateFunc is the core implementation of the `prefixLines` -// template function. -func prefixLinesTemplateFunc(prefix, s string) string { - type stateType int - const ( - startOfLine stateType = iota - inLine - ) + maps.Copy(funcMap, template.FuncMap{ + "lookPath": utils.EachStringErr(lookPathTemplateFunc), + "lstat": utils.EachString(lstatTemplateFunc), + "stat": utils.EachString(statTemplateFunc), + }) - state := startOfLine - var builder strings.Builder - builder.Grow(2 * len(s)) - for _, r := range s { - switch state { - case startOfLine: - if _, err := builder.WriteString(prefix); err != nil { - panic(err) - } - if _, err := builder.WriteRune(r); err != nil { - panic(err) - } - if r != '\n' { - state = inLine - } - case inLine: - if _, err := builder.WriteRune(r); err != nil { - panic(err) - } - if r == '\n' { - state = startOfLine - } - } - } - return builder.String() -} + maps.Copy(funcMap, booleanfuncs.FuncMap) + maps.Copy(funcMap, conversionfuncs.FuncMap) + maps.Copy(funcMap, listfuncs.FuncMap) + maps.Copy(funcMap, stringfuncs.FuncMap) -// eqFoldTemplateFunc is the core implementation of the `eqFold` template -// function. -func eqFoldTemplateFunc(first, second string, more ...string) bool { - if strings.EqualFold(first, second) { - return true - } - for _, s := range more { - if strings.EqualFold(first, s) { - return true - } - } - return false -} - -// fromJSONTemplateFunc is the core implementation of the `fromJSON` template -// function. -func fromJSONTemplateFunc(data []byte) (any, error) { - var result any - if err := json.Unmarshal(data, &result); err != nil { - return nil, err - } - return result, nil -} - -// listTemplateFunc is the core implementation of the `list` template function. -func listTemplateFunc(args ...any) []any { - return args + return funcMap } // lookPathTemplateFunc is the core implementation of the `lookPath` template @@ -137,7 +54,7 @@ func lookPathTemplateFunc(file string) (string, error) { func lstatTemplateFunc(name string) any { switch fileInfo, err := os.Lstat(name); { case err == nil: - return fileInfoToMap(fileInfo) + return utils.FileInfoToMap(fileInfo) case errors.Is(err, fs.ErrNotExist): return nil default: @@ -145,235 +62,14 @@ func lstatTemplateFunc(name string) any { } } -// regexpReplaceAllTemplateFunc is the core implementation of the -// `regexpReplaceAll` template function. -func regexpReplaceAllTemplateFunc(expr, repl, s string) string { - return regexp.MustCompile(expr).ReplaceAllString(s, repl) -} - // statTemplateFunc is the core implementation of the `stat` template function. func statTemplateFunc(name string) any { switch fileInfo, err := os.Stat(name); { case err == nil: - return fileInfoToMap(fileInfo) + return utils.FileInfoToMap(fileInfo) case errors.Is(err, fs.ErrNotExist): return nil default: panic(err) } } - -// toJSONTemplateFunc is the core implementation of the `toJSON` template -// function. -func toJSONTemplateFunc(arg any) []byte { - data, err := json.Marshal(arg) - if err != nil { - panic(err) - } - return data -} - -// toStringTemplateFunc is the core implementation of the `toString` template -// function. -func toStringTemplateFunc(arg any) string { - // FIXME add more types - switch arg := arg.(type) { - case string: - return arg - case []byte: - return string(arg) - case bool: - return strconv.FormatBool(arg) - case float32: - return strconv.FormatFloat(float64(arg), 'f', -1, 32) - case float64: - return strconv.FormatFloat(arg, 'f', -1, 64) - case int: - return strconv.Itoa(arg) - case int32: - return strconv.FormatInt(int64(arg), 10) - case int64: - return strconv.FormatInt(arg, 10) - default: - panic(fmt.Sprintf("%T: unsupported type", arg)) - } -} - -// eachByteSlice transforms a function that takes a single `[]byte` and returns -// a `T` to a function that takes zero or more `[]byte`-like arguments and -// returns zero or more `T`s. -func eachByteSlice[T any](f func([]byte) T) func(any) any { - return func(arg any) any { - switch arg := arg.(type) { - case []byte: - return f(arg) - case [][]byte: - result := make([]T, 0, len(arg)) - for _, a := range arg { - result = append(result, f(a)) - } - return result - case string: - return f([]byte(arg)) - case []string: - result := make([]T, 0, len(arg)) - for _, a := range arg { - result = append(result, f([]byte(a))) - } - return result - default: - panic(fmt.Sprintf("%T: unsupported argument type", arg)) - } - } -} - -// eachByteSliceErr transforms a function that takes a single `[]byte` and -// returns a `T` and an `error` into a function that takes zero or more -// `[]byte`-like arguments and returns zero or more `Ts` and an error. -func eachByteSliceErr[T any](f func([]byte) (T, error)) func(any) any { - return func(arg any) any { - switch arg := arg.(type) { - case []byte: - result, err := f(arg) - if err != nil { - panic(err) - } - return result - case [][]byte: - result := make([]T, 0, len(arg)) - for _, a := range arg { - r, err := f(a) - if err != nil { - panic(err) - } - result = append(result, r) - } - return result - case string: - result, err := f([]byte(arg)) - if err != nil { - panic(err) - } - return result - case []string: - result := make([]T, 0, len(arg)) - for _, a := range arg { - r, err := f([]byte(a)) - if err != nil { - panic(err) - } - result = append(result, r) - } - return result - default: - panic(fmt.Sprintf("%T: unsupported argument type", arg)) - } - } -} - -// eachString transforms a function that takes a single `string`-like argument -// and returns a `T` into a function that takes zero or more `string`-like -// arguments and returns zero or more `T`s. -func eachString[T any](f func(string) T) func(any) any { - return func(arg any) any { - switch arg := arg.(type) { - case string: - return f(arg) - case []string: - result := make([]T, 0, len(arg)) - for _, a := range arg { - result = append(result, f(a)) - } - return result - case []byte: - return f(string(arg)) - case [][]byte: - result := make([]T, 0, len(arg)) - for _, a := range arg { - result = append(result, f(string(a))) - } - return result - case []any: - result := make([]T, 0, len(arg)) - for _, a := range arg { - switch a := a.(type) { - case string: - result = append(result, f(a)) - case []byte: - result = append(result, f(string(a))) - default: - panic(fmt.Sprintf("%T: unsupported argument type", a)) - } - } - return result - default: - panic(fmt.Sprintf("%T: unsupported argument type", arg)) - } - } -} - -// eachStringErr transforms a function that takes a single `string`-like argument -// and returns a `T` and an `error` into a function that takes zero or more -// `string`-like arguments and returns zero or more `T`s and an `error`. -func eachStringErr[T any](f func(string) (T, error)) func(any) any { - return func(arg any) any { - switch arg := arg.(type) { - case string: - result, err := f(arg) - if err != nil { - panic(err) - } - return result - case []string: - result := make([]T, 0, len(arg)) - for _, a := range arg { - r, err := f(a) - if err != nil { - panic(err) - } - result = append(result, r) - } - return result - case []byte: - result, err := f(string(arg)) - if err != nil { - panic(err) - } - return result - case [][]byte: - result := make([]T, 0, len(arg)) - for _, a := range arg { - r, err := f(string(a)) - if err != nil { - panic(err) - } - result = append(result, r) - } - return result - default: - panic(fmt.Sprintf("%T: unsupported argument type", arg)) - } - } -} - -// fileInfoToMap returns a `map[string]any` of `fileInfo`'s fields. -func fileInfoToMap(fileInfo fs.FileInfo) map[string]any { - return map[string]any{ - "name": fileInfo.Name(), - "size": fileInfo.Size(), - "mode": int(fileInfo.Mode()), - "perm": int(fileInfo.Mode().Perm()), - "modTime": fileInfo.ModTime().Unix(), - "isDir": fileInfo.IsDir(), - "type": fileModeTypeNames[fileInfo.Mode()&fs.ModeType], - } -} - -// reverseArgs2 transforms a function that takes two arguments and returns an -// `R` into a function that takes the arguments in reverse order and returns an -// `R`. -func reverseArgs2[T1, T2, R any](f func(T1, T2) R) func(T2, T1) R { - return func(arg1 T2, arg2 T1) R { - return f(arg2, arg1) - } -} diff --git a/templatefuncs_test.go b/templatefuncs_test.go index 72029dd..4375331 100644 --- a/templatefuncs_test.go +++ b/templatefuncs_test.go @@ -7,6 +7,8 @@ import ( "text/template" "github.com/alecthomas/assert/v2" + + "github.com/chezmoi/templatefuncs/internal/utils" ) func TestEachString(t *testing.T) { @@ -37,7 +39,7 @@ func TestEachString(t *testing.T) { }, } { t.Run(strconv.Itoa(i), func(t *testing.T) { - f := eachString(tc.f) + f := utils.EachString(tc.f) assert.Equal(t, tc.expected, f(tc.arg)) }) } From e4943fcf22bc55305d289620a70c574174f73a19 Mon Sep 17 00:00:00 2001 From: Braden Hilton Date: Thu, 16 May 2024 02:14:08 +0100 Subject: [PATCH 2/2] a different approach --- .golangci.yml | 6 +- .../booleanfuncs.go | 6 +- conversionfuncs/conversionfuncs.go | 39 +++++++++++ docs/conversionfuncs.md | 38 ----------- docs/docs.go | 16 ++--- docs/encodingfuncs.md | 39 +++++++++++ encodingfuncs/encodingfuncs.go | 38 +++++++++++ .../utils.go => transform/transform.go} | 2 +- {pkg/listfuncs => listfuncs}/listfuncs.go | 12 ++-- pkg/conversionfuncs/conversionfuncs.go | 65 ------------------- pkg/stringfuncs/stringfuncs.go | 27 -------- stringfuncs/stringfuncs.go | 29 +++++++++ templatefuncs.go | 32 ++++----- templatefuncs_test.go | 4 +- 14 files changed, 185 insertions(+), 168 deletions(-) rename {pkg/booleanfuncs => booleanfuncs}/booleanfuncs.go (79%) create mode 100644 conversionfuncs/conversionfuncs.go create mode 100644 docs/encodingfuncs.md create mode 100644 encodingfuncs/encodingfuncs.go rename internal/{utils/utils.go => transform/transform.go} (99%) rename {pkg/listfuncs => listfuncs}/listfuncs.go (78%) delete mode 100644 pkg/conversionfuncs/conversionfuncs.go delete mode 100644 pkg/stringfuncs/stringfuncs.go create mode 100644 stringfuncs/stringfuncs.go diff --git a/.golangci.yml b/.golangci.yml index 5e62d94..9d6ccad 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -129,14 +129,14 @@ issues: - linters: - dupl - scopelint - path: "_test\\.go" + path: _test\.go - linters: - forbidigo - gosec - path: "internal/" + path: internal/ - linters: - err113 text: do not define dynamic errors, use wrapped static errors instead - linters: - gochecknoinits - path: "docs/docs.go" + path: docs\.go diff --git a/pkg/booleanfuncs/booleanfuncs.go b/booleanfuncs/booleanfuncs.go similarity index 79% rename from pkg/booleanfuncs/booleanfuncs.go rename to booleanfuncs/booleanfuncs.go index c32069b..866b5c9 100644 --- a/pkg/booleanfuncs/booleanfuncs.go +++ b/booleanfuncs/booleanfuncs.go @@ -5,8 +5,10 @@ import ( "text/template" ) -var FuncMap = template.FuncMap{ - "eqFold": eqFoldTemplateFunc, +func NewFuncMap() template.FuncMap { + return template.FuncMap{ + "eqFold": eqFoldTemplateFunc, + } } // eqFoldTemplateFunc is the core implementation of the `eqFold` template diff --git a/conversionfuncs/conversionfuncs.go b/conversionfuncs/conversionfuncs.go new file mode 100644 index 0000000..89bd1ae --- /dev/null +++ b/conversionfuncs/conversionfuncs.go @@ -0,0 +1,39 @@ +package conversionfuncs + +import ( + "fmt" + "strconv" + "text/template" +) + +func NewFuncMap() template.FuncMap { + return template.FuncMap{ + "toString": toStringTemplateFunc, + } +} + +// toStringTemplateFunc is the core implementation of the `toString` template +// function. +func toStringTemplateFunc(arg any) string { + // FIXME add more types + switch arg := arg.(type) { + case string: + return arg + case []byte: + return string(arg) + case bool: + return strconv.FormatBool(arg) + case float32: + return strconv.FormatFloat(float64(arg), 'f', -1, 32) + case float64: + return strconv.FormatFloat(arg, 'f', -1, 64) + case int: + return strconv.Itoa(arg) + case int32: + return strconv.FormatInt(int64(arg), 10) + case int64: + return strconv.FormatInt(arg, 10) + default: + panic(fmt.Sprintf("%T: unsupported type", arg)) + } +} diff --git a/docs/conversionfuncs.md b/docs/conversionfuncs.md index 0e2bb02..76c2fa3 100644 --- a/docs/conversionfuncs.md +++ b/docs/conversionfuncs.md @@ -1,43 +1,5 @@ # Conversion Functions -## `fromJSON` *jsontext* - -`fromJSON` parses *jsontext* as JSON and returns the parsed value. - -```text -{{ `{ "foo": "bar" }` | fromJSON }} -``` - -## `hexDecode` *hextext* - -`hexDecode` returns the bytes represented by *hextext*. - -```text -{{ hexDecode "666f6f626172" }} - -foobar -``` - -## `hexEncode` *string* - -`hexEncode` returns the hexadecimal encoding of *string*. - -```text -{{ hexEncode "foobar" }} - -666f6f626172 -``` - -## `toJSON` *input* - -`toJSON` returns a JSON string representation of *input*. - -```text -{{ list "foo" "bar" "baz" }} - -["foo","bar","baz"] -``` - ## `toString` *input* `toString` returns the string representation of *input*. diff --git a/docs/docs.go b/docs/docs.go index 738424a..4e6be32 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -3,7 +3,6 @@ package docs import ( "embed" "log" - "maps" "regexp" "strings" ) @@ -37,9 +36,6 @@ func readFiles() []string { } for _, fileInfo := range fileInfos { - if fileInfo.IsDir() || !strings.HasSuffix(fileInfo.Name(), ".md") { - continue - } content, err := f.ReadFile(fileInfo.Name()) if err != nil { log.Fatal(err) @@ -51,7 +47,6 @@ func readFiles() []string { } func parseFile(file string) map[string]Reference { - references := make(map[string]Reference) var reference Reference var funcName string var b strings.Builder @@ -59,13 +54,14 @@ func parseFile(file string) map[string]Reference { inExample := false lines := newlineRx.Split(file, -1) - funcType, lines := pageTitleRx.FindStringSubmatch(lines[0])[1], lines[1:] + funcType := pageTitleRx.FindStringSubmatch(lines[0])[1] + lines = lines[1:] for _, line := range lines { switch { case strings.HasPrefix(line, "## "): if reference.Title != "" { - references[funcName] = reference + References[funcName] = reference } funcName = funcNameRx.FindStringSubmatch(line)[1] reference = Reference{} @@ -94,10 +90,10 @@ func parseFile(file string) map[string]Reference { } if reference.Title != "" { - references[funcName] = reference + References[funcName] = reference } - return references + return References } func init() { @@ -106,6 +102,6 @@ func init() { files := readFiles() for _, file := range files { - maps.Copy(References, parseFile(file)) + parseFile(file) } } diff --git a/docs/encodingfuncs.md b/docs/encodingfuncs.md new file mode 100644 index 0000000..8f66b50 --- /dev/null +++ b/docs/encodingfuncs.md @@ -0,0 +1,39 @@ +# Encoding Functions + +## `fromJSON` *jsontext* + +`fromJSON` parses *jsontext* as JSON and returns the parsed value. + +```text +{{ `{ "foo": "bar" }` | fromJSON }} +``` + +## `hexDecode` *hextext* + +`hexDecode` returns the bytes represented by *hextext*. + +```text +{{ hexDecode "666f6f626172" }} + +foobar +``` + +## `hexEncode` *string* + +`hexEncode` returns the hexadecimal encoding of *string*. + +```text +{{ hexEncode "foobar" }} + +666f6f626172 +``` + +## `toJSON` *input* + +`toJSON` returns a JSON string representation of *input*. + +```text +{{ list "foo" "bar" "baz" }} + +["foo","bar","baz"] +``` diff --git a/encodingfuncs/encodingfuncs.go b/encodingfuncs/encodingfuncs.go new file mode 100644 index 0000000..e04eac5 --- /dev/null +++ b/encodingfuncs/encodingfuncs.go @@ -0,0 +1,38 @@ +package encodingfuncs + +import ( + "encoding/hex" + "encoding/json" + "text/template" + + "github.com/chezmoi/templatefuncs/internal/transform" +) + +func NewFuncMap() template.FuncMap { + return template.FuncMap{ + "fromJSON": transform.EachByteSliceErr(fromJSONTemplateFunc), + "hexDecode": transform.EachStringErr(hex.DecodeString), + "hexEncode": transform.EachByteSlice(hex.EncodeToString), + "toJSON": toJSONTemplateFunc, + } +} + +// fromJSONTemplateFunc is the core implementation of the `fromJSON` template +// function. +func fromJSONTemplateFunc(data []byte) (any, error) { + var result any + if err := json.Unmarshal(data, &result); err != nil { + return nil, err + } + return result, nil +} + +// toJSONTemplateFunc is the core implementation of the `toJSON` template +// function. +func toJSONTemplateFunc(arg any) []byte { + data, err := json.Marshal(arg) + if err != nil { + panic(err) + } + return data +} diff --git a/internal/utils/utils.go b/internal/transform/transform.go similarity index 99% rename from internal/utils/utils.go rename to internal/transform/transform.go index 3f4ac84..39e9e3e 100644 --- a/internal/utils/utils.go +++ b/internal/transform/transform.go @@ -1,4 +1,4 @@ -package utils +package transform import ( "fmt" diff --git a/pkg/listfuncs/listfuncs.go b/listfuncs/listfuncs.go similarity index 78% rename from pkg/listfuncs/listfuncs.go rename to listfuncs/listfuncs.go index add00a1..7848ab1 100644 --- a/pkg/listfuncs/listfuncs.go +++ b/listfuncs/listfuncs.go @@ -4,13 +4,15 @@ import ( "strings" "text/template" - "github.com/chezmoi/templatefuncs/internal/utils" + "github.com/chezmoi/templatefuncs/internal/transform" ) -var FuncMap = template.FuncMap{ - "join": utils.ReverseArgs2(strings.Join), - "list": listTemplateFunc, - "prefixLines": prefixLinesTemplateFunc, +func NewFuncMap() template.FuncMap { + return template.FuncMap{ + "join": transform.ReverseArgs2(strings.Join), + "list": listTemplateFunc, + "prefixLines": prefixLinesTemplateFunc, + } } // listTemplateFunc is the core implementation of the `list` template function. diff --git a/pkg/conversionfuncs/conversionfuncs.go b/pkg/conversionfuncs/conversionfuncs.go deleted file mode 100644 index 80a8724..0000000 --- a/pkg/conversionfuncs/conversionfuncs.go +++ /dev/null @@ -1,65 +0,0 @@ -package conversionfuncs - -import ( - "encoding/hex" - "encoding/json" - "fmt" - "strconv" - "text/template" - - "github.com/chezmoi/templatefuncs/internal/utils" -) - -var FuncMap = template.FuncMap{ - "fromJSON": utils.EachByteSliceErr(fromJSONTemplateFunc), - "hexDecode": utils.EachStringErr(hex.DecodeString), - "hexEncode": utils.EachByteSlice(hex.EncodeToString), - "toJSON": toJSONTemplateFunc, - "toString": toStringTemplateFunc, -} - -// fromJSONTemplateFunc is the core implementation of the `fromJSON` template -// function. -func fromJSONTemplateFunc(data []byte) (any, error) { - var result any - if err := json.Unmarshal(data, &result); err != nil { - return nil, err - } - return result, nil -} - -// toJSONTemplateFunc is the core implementation of the `toJSON` template -// function. -func toJSONTemplateFunc(arg any) []byte { - data, err := json.Marshal(arg) - if err != nil { - panic(err) - } - return data -} - -// toStringTemplateFunc is the core implementation of the `toString` template -// function. -func toStringTemplateFunc(arg any) string { - // FIXME add more types - switch arg := arg.(type) { - case string: - return arg - case []byte: - return string(arg) - case bool: - return strconv.FormatBool(arg) - case float32: - return strconv.FormatFloat(float64(arg), 'f', -1, 32) - case float64: - return strconv.FormatFloat(arg, 'f', -1, 64) - case int: - return strconv.Itoa(arg) - case int32: - return strconv.FormatInt(int64(arg), 10) - case int64: - return strconv.FormatInt(arg, 10) - default: - panic(fmt.Sprintf("%T: unsupported type", arg)) - } -} diff --git a/pkg/stringfuncs/stringfuncs.go b/pkg/stringfuncs/stringfuncs.go deleted file mode 100644 index 80cb083..0000000 --- a/pkg/stringfuncs/stringfuncs.go +++ /dev/null @@ -1,27 +0,0 @@ -package stringfuncs - -import ( - "regexp" - "strconv" - "strings" - "text/template" - - "github.com/chezmoi/templatefuncs/internal/utils" -) - -var FuncMap = template.FuncMap{ - "contains": utils.ReverseArgs2(strings.Contains), - "hasPrefix": utils.ReverseArgs2(strings.HasPrefix), - "hasSuffix": utils.ReverseArgs2(strings.HasSuffix), - "quote": utils.EachString(strconv.Quote), - "regexpReplaceAll": regexpReplaceAllTemplateFunc, - "toLower": utils.EachString(strings.ToLower), - "toUpper": utils.EachString(strings.ToUpper), - "trimSpace": utils.EachString(strings.TrimSpace), -} - -// regexpReplaceAllTemplateFunc is the core implementation of the -// `regexpReplaceAll` template function. -func regexpReplaceAllTemplateFunc(expr, repl, s string) string { - return regexp.MustCompile(expr).ReplaceAllString(s, repl) -} diff --git a/stringfuncs/stringfuncs.go b/stringfuncs/stringfuncs.go new file mode 100644 index 0000000..08835ac --- /dev/null +++ b/stringfuncs/stringfuncs.go @@ -0,0 +1,29 @@ +package stringfuncs + +import ( + "regexp" + "strconv" + "strings" + "text/template" + + "github.com/chezmoi/templatefuncs/internal/transform" +) + +func NewFuncMap() template.FuncMap { + return template.FuncMap{ + "contains": transform.ReverseArgs2(strings.Contains), + "hasPrefix": transform.ReverseArgs2(strings.HasPrefix), + "hasSuffix": transform.ReverseArgs2(strings.HasSuffix), + "quote": transform.EachString(strconv.Quote), + "regexpReplaceAll": regexpReplaceAllTemplateFunc, + "toLower": transform.EachString(strings.ToLower), + "toUpper": transform.EachString(strings.ToUpper), + "trimSpace": transform.EachString(strings.TrimSpace), + } +} + +// regexpReplaceAllTemplateFunc is the core implementation of the +// `regexpReplaceAll` template function. +func regexpReplaceAllTemplateFunc(expr, repl, s string) string { + return regexp.MustCompile(expr).ReplaceAllString(s, repl) +} diff --git a/templatefuncs.go b/templatefuncs.go index a5f3051..506cd94 100644 --- a/templatefuncs.go +++ b/templatefuncs.go @@ -8,28 +8,30 @@ import ( "os/exec" "text/template" - "github.com/chezmoi/templatefuncs/internal/utils" - "github.com/chezmoi/templatefuncs/pkg/booleanfuncs" - "github.com/chezmoi/templatefuncs/pkg/conversionfuncs" - "github.com/chezmoi/templatefuncs/pkg/listfuncs" - "github.com/chezmoi/templatefuncs/pkg/stringfuncs" + "github.com/chezmoi/templatefuncs/booleanfuncs" + "github.com/chezmoi/templatefuncs/conversionfuncs" + "github.com/chezmoi/templatefuncs/encodingfuncs" + "github.com/chezmoi/templatefuncs/internal/transform" + "github.com/chezmoi/templatefuncs/listfuncs" + "github.com/chezmoi/templatefuncs/stringfuncs" ) // NewFuncMap returns a new [text/template.FuncMap] containing all template // functions. func NewFuncMap() template.FuncMap { - funcMap := template.FuncMap{} + funcMap := make(template.FuncMap) maps.Copy(funcMap, template.FuncMap{ - "lookPath": utils.EachStringErr(lookPathTemplateFunc), - "lstat": utils.EachString(lstatTemplateFunc), - "stat": utils.EachString(statTemplateFunc), + "lookPath": transform.EachStringErr(lookPathTemplateFunc), + "lstat": transform.EachString(lstatTemplateFunc), + "stat": transform.EachString(statTemplateFunc), }) - maps.Copy(funcMap, booleanfuncs.FuncMap) - maps.Copy(funcMap, conversionfuncs.FuncMap) - maps.Copy(funcMap, listfuncs.FuncMap) - maps.Copy(funcMap, stringfuncs.FuncMap) + maps.Copy(funcMap, booleanfuncs.NewFuncMap()) + maps.Copy(funcMap, conversionfuncs.NewFuncMap()) + maps.Copy(funcMap, encodingfuncs.NewFuncMap()) + maps.Copy(funcMap, listfuncs.NewFuncMap()) + maps.Copy(funcMap, stringfuncs.NewFuncMap()) return funcMap } @@ -54,7 +56,7 @@ func lookPathTemplateFunc(file string) (string, error) { func lstatTemplateFunc(name string) any { switch fileInfo, err := os.Lstat(name); { case err == nil: - return utils.FileInfoToMap(fileInfo) + return transform.FileInfoToMap(fileInfo) case errors.Is(err, fs.ErrNotExist): return nil default: @@ -66,7 +68,7 @@ func lstatTemplateFunc(name string) any { func statTemplateFunc(name string) any { switch fileInfo, err := os.Stat(name); { case err == nil: - return utils.FileInfoToMap(fileInfo) + return transform.FileInfoToMap(fileInfo) case errors.Is(err, fs.ErrNotExist): return nil default: diff --git a/templatefuncs_test.go b/templatefuncs_test.go index 4375331..c3bf79b 100644 --- a/templatefuncs_test.go +++ b/templatefuncs_test.go @@ -8,7 +8,7 @@ import ( "github.com/alecthomas/assert/v2" - "github.com/chezmoi/templatefuncs/internal/utils" + "github.com/chezmoi/templatefuncs/internal/transform" ) func TestEachString(t *testing.T) { @@ -39,7 +39,7 @@ func TestEachString(t *testing.T) { }, } { t.Run(strconv.Itoa(i), func(t *testing.T) { - f := utils.EachString(tc.f) + f := transform.EachString(tc.f) assert.Equal(t, tc.expected, f(tc.arg)) }) }