From c0e867627d51b6d43cb0b84bcd2fcfbcb7c85b95 Mon Sep 17 00:00:00 2001 From: Atomys Date: Fri, 29 Mar 2024 15:12:11 +0100 Subject: [PATCH] =?UTF-8?q?chore:=20migrate=20to=20sprout=20=F0=9F=8C=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .devcontainer/devcontainer.json | 54 ++++ .github/workflows/test.yaml | 6 +- .vscode/launch.json | 15 ++ Makefile | 9 +- README.md | 116 ++++----- crypto.go | 42 ++- crypto_test.go | 2 +- date.go | 2 +- date_test.go | 2 +- defaults.go | 2 +- defaults_test.go | 2 +- dict.go | 4 +- dict_test.go | 2 +- doc.go | 10 +- example_test.go | 4 +- flow_control_test.go | 2 +- functions_linux_test.go | 2 +- functions_test.go | 19 +- functions_windows_test.go | 2 +- go.mod | 31 ++- go.sum | 89 +++---- issue_188_test.go | 2 +- list.go | 2 +- list_test.go | 2 +- network.go | 2 +- network_test.go | 2 +- numeric.go | 4 +- numeric_test.go | 5 +- reflect.go | 2 +- reflect_test.go | 6 +- regex.go | 2 +- regex_test.go | 2 +- semver.go | 2 +- semver_test.go | 2 +- functions.go => sprout.go | 183 ++++++------- strings.go | 445 +++++++++++++++++++++++--------- strings_test.go | 2 +- url.go | 2 +- url_test.go | 2 +- 39 files changed, 685 insertions(+), 401 deletions(-) create mode 100644 .devcontainer/devcontainer.json create mode 100644 .vscode/launch.json rename functions.go => sprout.go (81%) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..37f81e7 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,54 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/go +{ + "name": "Sprout", + "image": "mcr.microsoft.com/devcontainers/go:1.22-bullseye", + // Configure tool-specific properties. + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + // Set *default* container specific settings.json values on container create. + "settings": { + "go.toolsManagement.checkForUpdates": "local", + "go.useLanguageServer": true, + "go.gopath": "/go", + "go.coverMode": "atomic", + "go.coverOnSave": true, + "go.disableConcurrentTests": true, + "editor.formatOnSave": true, + "go.lintTool": "golangci-lint", + "editor.tabSize": 2, + "editor.renderWhitespace": "all", + "gopls": { + "ui.completion.usePlaceholders": true, + // Experimental settings + "completeUnimported": true, // autocomplete unimported packages + "deepCompletion": true, // enable deep completion + "staticcheck": true + }, + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit", + "source.fixAll": "explicit" + }, + "git.autofetch": true, + "files.autoGuessEncoding": true, + "files.encoding": "utf8", + "go.delveConfig": { + "apiVersion": 2, + "showGlobalVariables": false + }, + "editor.rulers": [80], + "search.useGlobalIgnoreFiles": true, + "search.useParentIgnoreFiles": true + }, + + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "golang.Go", + "aaron-bond.better-comments", + "jasonnutter.vscode-codeowners", + "jinliming2.vscode-go-template" + ] + } + } +} diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 9331cae..b168f9b 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -4,7 +4,7 @@ jobs: test: strategy: matrix: - go-version: [1.17.x, 1.18.x, 1.19.x] + go-version: [1.19.x, 1.20.x, 1.21.x, 1.22.x] platform: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.platform }} steps: @@ -15,6 +15,4 @@ jobs: - name: Checkout code uses: actions/checkout@v3 - name: Test - env: - GO111MODULE: on - run: go test -cover . + run: make test diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..1bc31dd --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch test function", + "type": "go", + "request": "launch", + "mode": "test", + "program": "${workspaceFolder}", + } + ] +} diff --git a/Makefile b/Makefile index 78d409c..7a1f90f 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,2 @@ -.PHONY: test test: - @echo "==> Running tests" - GO111MODULE=on go test -v - -.PHONY: test-cover -test-cover: - @echo "==> Running Tests with coverage" - GO111MODULE=on go test -cover . + go test ./... diff --git a/README.md b/README.md index 3e22c60..7647690 100644 --- a/README.md +++ b/README.md @@ -1,100 +1,80 @@ -# Sprig: Template functions for Go templates +# Sprout 🌱 -[![GoDoc](https://img.shields.io/static/v1?label=godoc&message=reference&color=blue)](https://pkg.go.dev/github.com/Masterminds/sprig/v3) -[![Go Report Card](https://goreportcard.com/badge/github.com/Masterminds/sprig)](https://goreportcard.com/report/github.com/Masterminds/sprig) -[![Stability: Sustained](https://masterminds.github.io/stability/sustained.svg)](https://masterminds.github.io/stability/sustained.html) -[![](https://github.com/Masterminds/sprig/workflows/Tests/badge.svg)](https://github.com/Masterminds/sprig/actions) +> [!NOTE] +> Sprout is an evolved variant of the [Masterminds/sprig](https://github.com/Masterminds/sprig) library, reimagined for modern Go versions. It introduces fresh functionalities and commits to maintaining the library, picking up where Sprig left off. Notably, Sprig had not seen updates for two years and was not compatible beyond Golang 1.13, necessitating the creation of Sprout. -The Go language comes with a [built-in template -language](http://golang.org/pkg/text/template/), but not -very many template functions. Sprig is a library that provides more than 100 commonly -used template functions. +# Table of Content -It is inspired by the template functions found in -[Twig](http://twig.sensiolabs.org/documentation) and in various -JavaScript libraries, such as [underscore.js](http://underscorejs.org/). -## IMPORTANT NOTES -Sprig leverages [mergo](https://github.com/imdario/mergo) to handle merges. In -its v0.3.9 release, there was a behavior change that impacts merging template -functions in sprig. It is currently recommended to use v0.3.10 or later of that package. -Using v0.3.9 will cause sprig tests to fail. +## Transitioning from Sprig -## Package Versions +For those looking to switch from Sprig to Sprout, the process is straightforward and involves just a couple of steps: +1. Ensure your project uses Sprig's last version (v3.2.3). +2. Update your import statements and package references as shown below: +```diff +import ( +- "github.com/Masterminds/sprig/v3" ++ "github.com/42atomys/sprout" -There are two active major versions of the `sprig` package. + "html/template" +) -* v3 is currently stable release series on the `master` branch. The Go API should - remain compatible with v2, the current stable version. Behavior change behind - some functions is the reason for the new major version. -* v2 is the previous stable release series. It has been more than three years since - the initial release of v2. You can read the documentation and see the code - on the [release-2](https://github.com/Masterminds/sprig/tree/release-2) branch. - Bug fixes to this major version will continue for some time. +tpl := template.Must( + template.New("base"). +- Funcs(sprig.FuncMap()). ++ Funcs(sprout.FuncMap()). + ParseGlob("*.html") +) +``` ## Usage -**Template developers**: Please use Sprig's [function documentation](http://masterminds.github.io/sprig/) for -detailed instructions and code snippets for the >100 template functions available. - -**Go developers**: If you'd like to include Sprig as a library in your program, -our API documentation is available [at GoDoc.org](http://godoc.org/github.com/Masterminds/sprig). +**For Template Creators**: Refer to the comprehensive function guide in Sprig's documentation for detailed instructions and examples across over 100 template functions. -For standard usage, read on. +**For Go Developers**: Integrate Sprout into your applications by consulting our API documentation available on GoDoc.org. -### Load the Sprig library +For general library usage, proceed as follows. -To load the Sprig `FuncMap`: +### Integrating the Sprout Library +To utilize Sprout's functions within your templates: -```go +```golang import ( - "github.com/Masterminds/sprig/v3" + "github.com/42atomys/sprout" "html/template" ) -// This example illustrates that the FuncMap *must* be set before the -// templates themselves are loaded. +// Ensure the FuncMap is set before loading the templates. tpl := template.Must( - template.New("base").Funcs(sprig.FuncMap()).ParseGlob("*.html") + template.New("base").Funcs(sprout.FuncMap()).ParseGlob("*.html") ) - - ``` -### Calling the functions inside of templates +### Template Function Invocation +Adhering to Go's conventions, all Sprout functions are lowercase, differing from method naming which employs TitleCase. For instance, this template snippet: -By convention, all functions are lowercase. This seems to follow the Go -idiom for template functions (as opposed to template methods, which are -TitleCase). For example, this: -``` +```golang {{ "hello!" | upper | repeat 5 }} ``` - -produces this: - +Will output: ``` HELLO!HELLO!HELLO!HELLO!HELLO! ``` -## Principles Driving Our Function Selection - -We followed these principles to decide which functions to add and how to implement them: - -- Use template functions to build layout. The following - types of operations are within the domain of template functions: - - Formatting - - Layout - - Simple type conversions - - Utilities that assist in handling common formatting and layout needs (e.g. arithmetic) -- Template functions should not return errors unless there is no way to print - a sensible value. For example, converting a string to an integer should not - produce an error if conversion fails. Instead, it should display a default - value. -- Simple math is necessary for grid layouts, pagers, and so on. Complex math - (anything other than arithmetic) should be done outside of templates. -- Template functions only deal with the data passed into them. They never retrieve - data from a source. -- Finally, do not override core Go template functions. +### Development Philosophy (Currently in reflexion to create our) + +Our approach to extending and refining Sprout was guided by several key principles: + +- Empowering layout construction through template functions. +- Designing template functions that avoid returning errors when possible, instead displaying default values for smoother user experiences. +- Ensuring template functions operate solely on provided data, without external data fetching. +- Maintaining the integrity of core Go template functionalities without overrides. + + + + + + diff --git a/crypto.go b/crypto.go index 13a5cd5..b1f15d5 100644 --- a/crypto.go +++ b/crypto.go @@ -1,4 +1,4 @@ -package sprig +package sprout import ( "bytes" @@ -36,6 +36,46 @@ import ( "golang.org/x/crypto/scrypt" ) +type cryptoRandomStringOpts struct { + letters bool + numbers bool + ascii bool + chars []rune +} + +func cryptoRandomString(count int, opts cryptoRandomStringOpts) string { + source := []rune{} + if opts.chars == nil { + if opts.ascii { + for i := 32; i <= 126; i++ { + source = append(source, rune(i)) + } + } + + if opts.letters { + for i := 'a'; i <= 'z'; i++ { + source = append(source, i) + } + for i := 'A'; i <= 'Z'; i++ { + source = append(source, i) + } + } + if opts.numbers { + for i := '0'; i <= '9'; i++ { + source = append(source, i) + } + } + } + + // Generate random string + return strings.Map(func(r rune) rune { + if bigInt, err := rand.Int(rand.Reader, big.NewInt(int64(len(source)))); err == nil { + return source[bigInt.Int64()] + } + return rune(-1) // Should not happen, indicates an error + }, strings.Repeat(" ", count)) +} + func sha256sum(input string) string { hash := sha256.Sum256([]byte(input)) return hex.EncodeToString(hash[:]) diff --git a/crypto_test.go b/crypto_test.go index 449e7ff..6487bb8 100644 --- a/crypto_test.go +++ b/crypto_test.go @@ -1,4 +1,4 @@ -package sprig +package sprout import ( "crypto/x509" diff --git a/date.go b/date.go index ed022dd..09b99b4 100644 --- a/date.go +++ b/date.go @@ -1,4 +1,4 @@ -package sprig +package sprout import ( "strconv" diff --git a/date_test.go b/date_test.go index be7ec9d..187c492 100644 --- a/date_test.go +++ b/date_test.go @@ -1,4 +1,4 @@ -package sprig +package sprout import ( "testing" diff --git a/defaults.go b/defaults.go index b9f9796..0fa4df7 100644 --- a/defaults.go +++ b/defaults.go @@ -1,4 +1,4 @@ -package sprig +package sprout import ( "bytes" diff --git a/defaults_test.go b/defaults_test.go index a35ebf6..d0a2c77 100644 --- a/defaults_test.go +++ b/defaults_test.go @@ -1,4 +1,4 @@ -package sprig +package sprout import ( "testing" diff --git a/dict.go b/dict.go index ade8896..5f33aa4 100644 --- a/dict.go +++ b/dict.go @@ -1,7 +1,7 @@ -package sprig +package sprout import ( - "github.com/imdario/mergo" + "dario.cat/mergo" "github.com/mitchellh/copystructure" ) diff --git a/dict_test.go b/dict_test.go index c829daa..96d9c06 100644 --- a/dict_test.go +++ b/dict_test.go @@ -1,4 +1,4 @@ -package sprig +package sprout import ( "strings" diff --git a/doc.go b/doc.go index aabb9d4..2d54b52 100644 --- a/doc.go +++ b/doc.go @@ -1,19 +1,19 @@ /* -Package sprig provides template functions for Go. +package sprout provides template functions for Go. This package contains a number of utility functions for working with data inside of Go `html/template` and `text/template` files. To add these functions, use the `template.Funcs()` method: - t := templates.New("foo").Funcs(sprig.FuncMap()) + t := templates.New("foo").Funcs(sprout.FuncMap()) Note that you should add the function map before you parse any template files. - In several cases, Sprig reverses the order of arguments from the way they + In several cases, sprout reverses the order of arguments from the way they appear in the standard library. This is to make it easier to pipe arguments into functions. -See http://masterminds.github.io/sprig/ for more detailed documentation on each of the available functions. +See http://masterminds.github.io/sprout/ for more detailed documentation on each of the available functions. */ -package sprig +package sprout diff --git a/example_test.go b/example_test.go index 2d7696b..343e34a 100644 --- a/example_test.go +++ b/example_test.go @@ -1,4 +1,4 @@ -package sprig +package sprout import ( "fmt" @@ -11,7 +11,7 @@ func Example() { vars := map[string]interface{}{"Name": " John Jacob Jingleheimer Schmidt "} tpl := `Hello {{.Name | trim | lower}}` - // Get the Sprig function map. + // Get the sprout function map. fmap := TxtFuncMap() t := template.Must(template.New("test").Funcs(fmap).Parse(tpl)) diff --git a/flow_control_test.go b/flow_control_test.go index d4e5ebf..c7c282b 100644 --- a/flow_control_test.go +++ b/flow_control_test.go @@ -1,4 +1,4 @@ -package sprig +package sprout import ( "fmt" diff --git a/functions_linux_test.go b/functions_linux_test.go index cfbf253..49d2477 100644 --- a/functions_linux_test.go +++ b/functions_linux_test.go @@ -1,4 +1,4 @@ -package sprig +package sprout import ( "testing" diff --git a/functions_test.go b/functions_test.go index af78976..30e8276 100644 --- a/functions_test.go +++ b/functions_test.go @@ -1,4 +1,4 @@ -package sprig +package sprout import ( "bytes" @@ -7,7 +7,6 @@ import ( "os" "testing" "text/template" - "time" "github.com/stretchr/testify/assert" ) @@ -56,6 +55,10 @@ func TestSnakeCase(t *testing.T) { assert.NoError(t, runt(`{{ snakecase "GO_PATH" }}`, "go_path")) assert.NoError(t, runt(`{{ snakecase "GO PATH" }}`, "go_path")) assert.NoError(t, runt(`{{ snakecase "GO-PATH" }}`, "go_path")) + assert.NoError(t, runt(`{{ snakecase "http2xx" }}`, "http_2xx")) + assert.NoError(t, runt(`{{ snakecase "HTTP20xOK" }}`, "http_20x_ok")) + assert.NoError(t, runt(`{{ snakecase "Duration2m3s" }}`, "duration_2m3s")) + assert.NoError(t, runt(`{{ snakecase "Bld4Floor3rd" }}`, "bld4_floor_3rd")) } func TestCamelCase(t *testing.T) { @@ -76,11 +79,15 @@ func TestKebabCase(t *testing.T) { } func TestShuffle(t *testing.T) { - defer rand.Seed(time.Now().UnixNano()) - rand.Seed(1) + originalRand := randSource + defer func() { + randSource = originalRand + }() + + randSource = rand.NewSource(42) // Because we're using a random number generator, we need these to go in // a predictable sequence: - assert.NoError(t, runt(`{{ shuffle "Hello World" }}`, "rldo HWlloe")) + assert.NoError(t, runt(`{{ shuffle "Hello World" }}`, "Wrlodlle Ho")) } func TestRegex(t *testing.T) { @@ -105,7 +112,7 @@ func runtv(tpl, expect string, vars interface{}) error { return err } if expect != b.String() { - return fmt.Errorf("Expected '%s', got '%s'", expect, b.String()) + return fmt.Errorf("Expected '%v', got '%v'", expect, b.String()) } return nil } diff --git a/functions_windows_test.go b/functions_windows_test.go index 9d8bd0e..9e1dfd2 100644 --- a/functions_windows_test.go +++ b/functions_windows_test.go @@ -1,4 +1,4 @@ -package sprig +package sprout import ( "testing" diff --git a/go.mod b/go.mod index 494916f..bcb1ae1 100644 --- a/go.mod +++ b/go.mod @@ -1,16 +1,23 @@ -module github.com/Masterminds/sprig/v3 +module github.com/42atomys/sprout -go 1.13 +go 1.19 require ( - github.com/Masterminds/goutils v1.1.1 - github.com/Masterminds/semver/v3 v3.2.0 - github.com/google/uuid v1.1.1 - github.com/huandu/xstrings v1.3.3 - github.com/imdario/mergo v0.3.11 - github.com/mitchellh/copystructure v1.0.0 - github.com/shopspring/decimal v1.2.0 - github.com/spf13/cast v1.3.1 - github.com/stretchr/testify v1.5.1 - golang.org/x/crypto v0.3.0 + dario.cat/mergo v1.0.0 + github.com/Masterminds/semver/v3 v3.2.1 + github.com/google/uuid v1.6.0 + github.com/huandu/xstrings v1.4.0 + github.com/mitchellh/copystructure v1.2.0 + github.com/shopspring/decimal v1.3.1 + github.com/spf13/cast v1.6.0 + github.com/stretchr/testify v1.9.0 + golang.org/x/crypto v0.21.0 + golang.org/x/text v0.14.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 11a6711..71e8634 100644 --- a/go.sum +++ b/go.sum @@ -1,61 +1,40 @@ -github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= -github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g= -github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= -github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= -github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= -github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= -github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= -github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= +github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= -github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= -github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A= -golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/issue_188_test.go b/issue_188_test.go index c159ffb..b04f38b 100644 --- a/issue_188_test.go +++ b/issue_188_test.go @@ -1,4 +1,4 @@ -package sprig +package sprout import ( "testing" diff --git a/list.go b/list.go index ca0fbb7..6f95cda 100644 --- a/list.go +++ b/list.go @@ -1,4 +1,4 @@ -package sprig +package sprout import ( "fmt" diff --git a/list_test.go b/list_test.go index ec4c4c1..fc28c16 100644 --- a/list_test.go +++ b/list_test.go @@ -1,4 +1,4 @@ -package sprig +package sprout import ( "testing" diff --git a/network.go b/network.go index 108d78a..3423686 100644 --- a/network.go +++ b/network.go @@ -1,4 +1,4 @@ -package sprig +package sprout import ( "math/rand" diff --git a/network_test.go b/network_test.go index 9c153f0..232814c 100644 --- a/network_test.go +++ b/network_test.go @@ -1,4 +1,4 @@ -package sprig +package sprout import ( "net" diff --git a/numeric.go b/numeric.go index f68e418..d8c0243 100644 --- a/numeric.go +++ b/numeric.go @@ -1,4 +1,4 @@ -package sprig +package sprout import ( "fmt" @@ -6,8 +6,8 @@ import ( "strconv" "strings" - "github.com/spf13/cast" "github.com/shopspring/decimal" + "github.com/spf13/cast" ) // toFloat64 converts 64-bit floats diff --git a/numeric_test.go b/numeric_test.go index 1252cd7..f398e45 100644 --- a/numeric_test.go +++ b/numeric_test.go @@ -1,10 +1,11 @@ -package sprig +package sprout import ( "fmt" - "github.com/stretchr/testify/assert" "strconv" "testing" + + "github.com/stretchr/testify/assert" ) func TestUntil(t *testing.T) { diff --git a/reflect.go b/reflect.go index 8a65c13..adffb58 100644 --- a/reflect.go +++ b/reflect.go @@ -1,4 +1,4 @@ -package sprig +package sprout import ( "fmt" diff --git a/reflect_test.go b/reflect_test.go index f102907..80f13bf 100644 --- a/reflect_test.go +++ b/reflect_test.go @@ -1,4 +1,4 @@ -package sprig +package sprout import ( "testing" @@ -11,7 +11,7 @@ type fixtureTO struct { func TestTypeOf(t *testing.T) { f := &fixtureTO{"hello", "world"} tpl := `{{typeOf .}}` - if err := runtv(tpl, "*sprig.fixtureTO", f); err != nil { + if err := runtv(tpl, "*sprout.fixtureTO", f); err != nil { t.Error(err) } } @@ -37,7 +37,7 @@ func TestKindOf(t *testing.T) { func TestTypeIs(t *testing.T) { f := &fixtureTO{"hello", "world"} - tpl := `{{if typeIs "*sprig.fixtureTO" .}}t{{else}}f{{end}}` + tpl := `{{if typeIs "*sprout.fixtureTO" .}}t{{else}}f{{end}}` if err := runtv(tpl, "t", f); err != nil { t.Error(err) } diff --git a/regex.go b/regex.go index fab5510..9c4b8db 100644 --- a/regex.go +++ b/regex.go @@ -1,4 +1,4 @@ -package sprig +package sprout import ( "regexp" diff --git a/regex_test.go b/regex_test.go index 60aafc2..6783909 100644 --- a/regex_test.go +++ b/regex_test.go @@ -1,4 +1,4 @@ -package sprig +package sprout import ( "testing" diff --git a/semver.go b/semver.go index 3fbe08a..97245ce 100644 --- a/semver.go +++ b/semver.go @@ -1,4 +1,4 @@ -package sprig +package sprout import ( sv2 "github.com/Masterminds/semver/v3" diff --git a/semver_test.go b/semver_test.go index 53d3c8b..d2f2245 100644 --- a/semver_test.go +++ b/semver_test.go @@ -1,4 +1,4 @@ -package sprig +package sprout import ( "testing" diff --git a/functions.go b/sprout.go similarity index 81% rename from functions.go rename to sprout.go index 57fcec1..01b0c42 100644 --- a/functions.go +++ b/sprout.go @@ -1,4 +1,4 @@ -package sprig +package sprout import ( "errors" @@ -13,70 +13,22 @@ import ( ttemplate "text/template" "time" - util "github.com/Masterminds/goutils" "github.com/huandu/xstrings" "github.com/shopspring/decimal" ) -// FuncMap produces the function map. -// -// Use this to pass the functions into the template engine: -// -// tpl := template.New("foo").Funcs(sprig.FuncMap())) -// -func FuncMap() template.FuncMap { - return HtmlFuncMap() -} - -// HermeticTxtFuncMap returns a 'text/template'.FuncMap with only repeatable functions. -func HermeticTxtFuncMap() ttemplate.FuncMap { - r := TxtFuncMap() - for _, name := range nonhermeticFunctions { - delete(r, name) - } - return r -} - -// HermeticHtmlFuncMap returns an 'html/template'.Funcmap with only repeatable functions. -func HermeticHtmlFuncMap() template.FuncMap { - r := HtmlFuncMap() - for _, name := range nonhermeticFunctions { - delete(r, name) - } - return r -} - -// TxtFuncMap returns a 'text/template'.FuncMap -func TxtFuncMap() ttemplate.FuncMap { - return ttemplate.FuncMap(GenericFuncMap()) -} - -// HtmlFuncMap returns an 'html/template'.Funcmap -func HtmlFuncMap() template.FuncMap { - return template.FuncMap(GenericFuncMap()) -} - -// GenericFuncMap returns a copy of the basic function map as a map[string]interface{}. -func GenericFuncMap() map[string]interface{} { - gfm := make(map[string]interface{}, len(genericMap)) - for k, v := range genericMap { - gfm[k] = v - } - return gfm -} - // These functions are not guaranteed to evaluate to the same result for given input, because they // refer to the environment or global state. var nonhermeticFunctions = []string{ // Date functions "date", "date_in_zone", + "dateInZone", "date_modify", + "dateModify", "now", "htmlDate", "htmlDateInZone", - "dateInZone", - "dateModify", // Strings "randAlphaNum", @@ -98,16 +50,19 @@ var genericMap = map[string]interface{}{ "hello": func() string { return "Hello!" }, // Date functions - "ago": dateAgo, - "date": date, - "date_in_zone": dateInZone, - "date_modify": dateModify, - "dateInZone": dateInZone, - "dateModify": dateModify, - "duration": duration, - "durationRound": durationRound, - "htmlDate": htmlDate, - "htmlDateInZone": htmlDateInZone, + "ago": dateAgo, + "date": date, + //! Deprecated: Should use dateModify instead + "date_modify": dateModify, + "dateModify": dateModify, + //! Deprecated: Should use dateInZone instead + "date_in_zone": dateInZone, + "dateInZone": dateInZone, + "duration": duration, + "durationRound": durationRound, + "htmlDate": htmlDate, + "htmlDateInZone": htmlDateInZone, + //! Deprecated: Should use mustDateModify instead "must_date_modify": mustDateModify, "mustDateModify": mustDateModify, "mustToDate": mustToDate, @@ -116,15 +71,19 @@ var genericMap = map[string]interface{}{ "unixEpoch": unixEpoch, // Strings - "abbrev": abbrev, - "abbrevboth": abbrevboth, - "trunc": trunc, - "trim": strings.TrimSpace, - "upper": strings.ToUpper, - "lower": strings.ToLower, - "title": strings.Title, - "untitle": untitle, - "substr": substring, + //! Deprecated: Should use ellipsis instead + "abbrev": func(width int, str string) string { return ellipsis(str, 0, width) }, + "ellipsis": func(width int, str string) string { return ellipsis(str, 0, width) }, + //! Deprecated: Should use ellipsisBoth instead + "abbrevboth": func(left, right int, str string) string { return ellipsis(str, left, right) }, + "ellipsisBoth": func(left, right int, str string) string { return ellipsis(str, left, right) }, + "trunc": trunc, + "trim": strings.TrimSpace, + "upper": strings.ToUpper, + "lower": strings.ToLower, + "title": toTitleCase, + "untitle": untitle, + "substr": substring, // Switch order so that "foo" | repeat 5 "repeat": func(count int, str string) string { return strings.Repeat(str, count) }, // Deprecated: Use trimAll. @@ -133,19 +92,19 @@ var genericMap = map[string]interface{}{ "trimAll": func(a, b string) string { return strings.Trim(b, a) }, "trimSuffix": func(a, b string) string { return strings.TrimSuffix(b, a) }, "trimPrefix": func(a, b string) string { return strings.TrimPrefix(b, a) }, - "nospace": util.DeleteWhiteSpace, - "initials": initials, + "nospace": nospace, + "initials": func(a string) string { return initials(a, "") }, "randAlphaNum": randAlphaNumeric, "randAlpha": randAlpha, "randAscii": randAscii, "randNumeric": randNumeric, - "swapcase": util.SwapCase, - "shuffle": xstrings.Shuffle, + "swapcase": swapCase, + "shuffle": shuffle, "snakecase": xstrings.ToSnakeCase, "camelcase": xstrings.ToCamelCase, "kebabcase": xstrings.ToKebabCase, - "wrap": func(l int, s string) string { return util.Wrap(s, l) }, - "wrapWith": func(l int, sep, str string) string { return util.WrapCustom(str, l, sep, true) }, + "wrap": func(l int, s string) string { return wordWrap(s, l, "", false) }, + "wrapWith": func(l int, sep, str string) string { return wordWrap(str, l, sep, true) }, // Switch order so that "foobar" | contains "foo" "contains": func(substr string, str string) bool { return strings.Contains(str, substr) }, "hasPrefix": func(substr string, str string) bool { return strings.HasPrefix(str, substr) }, @@ -336,20 +295,20 @@ var genericMap = map[string]interface{}{ "mustChunk": mustChunk, // Crypto: - "bcrypt": bcrypt, - "htpasswd": htpasswd, - "genPrivateKey": generatePrivateKey, - "derivePassword": derivePassword, - "buildCustomCert": buildCustomCertificate, - "genCA": generateCertificateAuthority, - "genCAWithKey": generateCertificateAuthorityWithPEMKey, - "genSelfSignedCert": generateSelfSignedCertificate, + "bcrypt": bcrypt, + "htpasswd": htpasswd, + "genPrivateKey": generatePrivateKey, + "derivePassword": derivePassword, + "buildCustomCert": buildCustomCertificate, + "genCA": generateCertificateAuthority, + "genCAWithKey": generateCertificateAuthorityWithPEMKey, + "genSelfSignedCert": generateSelfSignedCertificate, "genSelfSignedCertWithKey": generateSelfSignedCertificateWithPEMKey, - "genSignedCert": generateSignedCertificate, - "genSignedCertWithKey": generateSignedCertificateWithPEMKey, - "encryptAES": encryptAES, - "decryptAES": decryptAES, - "randBytes": randBytes, + "genSignedCert": generateSignedCertificate, + "genSignedCertWithKey": generateSignedCertificateWithPEMKey, + "encryptAES": encryptAES, + "decryptAES": decryptAES, + "randBytes": randBytes, // UUIDs: "uuidv4": uuidv4, @@ -380,3 +339,49 @@ var genericMap = map[string]interface{}{ "urlParse": urlParse, "urlJoin": urlJoin, } + +// FuncMap produces the function map. +// +// Use this to pass the functions into the template engine: +// +// tpl := template.New("foo").Funcs(sprout.FuncMap())) +func FuncMap() template.FuncMap { + return HtmlFuncMap() +} + +// HermeticTxtFuncMap returns a 'text/template'.FuncMap with only repeatable functions. +func HermeticTxtFuncMap() ttemplate.FuncMap { + r := TxtFuncMap() + for _, name := range nonhermeticFunctions { + delete(r, name) + } + return r +} + +// HermeticHtmlFuncMap returns an 'html/template'.Funcmap with only repeatable functions. +func HermeticHtmlFuncMap() template.FuncMap { + r := HtmlFuncMap() + for _, name := range nonhermeticFunctions { + delete(r, name) + } + return r +} + +// TxtFuncMap returns a 'text/template'.FuncMap +func TxtFuncMap() ttemplate.FuncMap { + return ttemplate.FuncMap(GenericFuncMap()) +} + +// HtmlFuncMap returns an 'html/template'.Funcmap +func HtmlFuncMap() template.FuncMap { + return template.FuncMap(GenericFuncMap()) +} + +// GenericFuncMap returns a copy of the basic function map as a map[string]interface{}. +func GenericFuncMap() map[string]interface{} { + gfm := make(map[string]interface{}, len(genericMap)) + for k, v := range genericMap { + gfm[k] = v + } + return gfm +} diff --git a/strings.go b/strings.go index e0ae628..87c11e6 100644 --- a/strings.go +++ b/strings.go @@ -1,116 +1,315 @@ -package sprig +package sprout import ( "encoding/base32" "encoding/base64" "fmt" + "math/rand" "reflect" - "strconv" "strings" + "time" + "unicode" + "unicode/utf8" - util "github.com/Masterminds/goutils" + "golang.org/x/text/cases" + "golang.org/x/text/language" ) -func base64encode(v string) string { - return base64.StdEncoding.EncodeToString([]byte(v)) +var randSource rand.Source + +func init() { + randSource = rand.NewSource(time.Now().UnixNano()) } -func base64decode(v string) string { - data, err := base64.StdEncoding.DecodeString(v) - if err != nil { - return err.Error() +func nospace(str string) string { + return strings.Map(func(r rune) rune { + if unicode.IsSpace(r) { + return -1 + } + return r + }, str) +} + +func swapCase(s string) string { + return strings.Map(func(r rune) rune { + switch { + case unicode.IsLower(r): + return unicode.ToUpper(r) + case unicode.IsUpper(r): + return unicode.ToLower(r) + default: + return r + } + }, s) +} + +func wordWrap(str string, wrapLength int, newLineCharacter string, wrapLongWords bool) string { + if wrapLength < 1 { + wrapLength = 1 } - return string(data) + if newLineCharacter == "" { + newLineCharacter = "\n" + } + + var resultBuilder strings.Builder + var currentLineLength int + + for _, word := range strings.Fields(str) { + wordLength := utf8.RuneCountInString(word) + + // If the word is too long and should be wrapped, or it fits in the remaining line length + if currentLineLength > 0 && (currentLineLength+1+wordLength > wrapLength && !wrapLongWords || wordLength > wrapLength) { + resultBuilder.WriteString(newLineCharacter) + currentLineLength = 0 + } + + if wrapLongWords && wordLength > wrapLength { + for i, r := range word { + if currentLineLength == wrapLength { + resultBuilder.WriteString(newLineCharacter) + currentLineLength = 0 + } + resultBuilder.WriteRune(r) + currentLineLength++ + // Avoid adding a new line immediately after wrapping a long word + if i < len(word)-1 && currentLineLength == wrapLength { + resultBuilder.WriteString(newLineCharacter) + currentLineLength = 0 + } + } + } else { + if currentLineLength > 0 { + resultBuilder.WriteString(newLineCharacter) + currentLineLength++ + } + resultBuilder.WriteString(word) + currentLineLength += wordLength + } + } + + return resultBuilder.String() } -func base32encode(v string) string { - return base32.StdEncoding.EncodeToString([]byte(v)) +func toTitleCase(s string) string { + return cases.Title(language.English).String(s) } -func base32decode(v string) string { - data, err := base32.StdEncoding.DecodeString(v) - if err != nil { - return err.Error() +// shuffle shuffles a string in a random manner. +func shuffle(str string) string { + r := []rune(str) + rand.New(randSource).Shuffle(len(r), func(i, j int) { + r[i], r[j] = r[j], r[i] + }) + return string(r) +} + +// ellipsis adds an ellipsis to the string `str` starting at `offset` if the length exceeds `maxWidth`. +// `maxWidth` must be at least 4, to accommodate the ellipsis and at least one character. +func ellipsis(str string, offset, maxWidth int) string { + ellipsis := "..." + // Return the original string if maxWidth is less than 4, or the offset + // create exclusive dot string, it's not possible to add an ellipsis. + if maxWidth < 4 || offset > 0 && maxWidth < 7 { + return str + } + + runeCount := utf8.RuneCountInString(str) + + // If the string doesn't need trimming, return it as is. + if runeCount <= maxWidth || runeCount <= offset { + return str[offset:] + } + + // Determine end position for the substring, ensuring room for the ellipsis. + endPos := offset + maxWidth - 3 // 3 is for the length of the ellipsis + if offset > 0 { + endPos -= 3 // remove the left ellipsis + } + + // Convert the string to a slice of runes to properly handle multi-byte characters. + runes := []rune(str) + + // Return the substring with an ellipsis, directly constructing the string in the return statement. + if offset > 0 { + return ellipsis + string(runes[offset:endPos]) + ellipsis } - return string(data) + return string(runes[offset:endPos]) + ellipsis } -func abbrev(width int, s string) string { - if width < 4 { - return s +// initials extracts the initials from the given string using the specified delimiters. +// If delimiters are empty, it defaults to using whitespace. +func initials(str string, delimiters string) string { + // Define a function to determine if a rune is a delimiter. + isDelimiter := func(r rune) bool { + if delimiters == "" { + return unicode.IsSpace(r) + } + return strings.ContainsRune(delimiters, r) + } + + words := strings.FieldsFunc(str, isDelimiter) + var runes = make([]rune, len(words)) + for i, word := range strings.FieldsFunc(str, isDelimiter) { + if i == 0 || unicode.IsLetter(rune(word[0])) { + runes[i] = rune(word[0]) + } } - r, _ := util.Abbreviate(s, width) - return r + + return string(runes) } -func abbrevboth(left, right int, s string) string { - if right < 4 || left > 0 && right < 7 { - return s +// uncapitalize transforms the first letter of each word in the string to lowercase. +// It uses specified delimiters or whitespace to determine word boundaries. +func uncapitalize(str string, delimiters string) string { + var result strings.Builder + // Convert delimiters to a map for efficient checking + delimMap := make(map[rune]bool) + for _, d := range delimiters { + delimMap[d] = true + } + + // Helper function to check if a rune is a delimiter + isDelim := func(r rune) bool { + if delimiters == "" { + return unicode.IsSpace(r) + } + return delimMap[r] + } + + // Process each rune in the input string + startOfWord := true + for _, r := range str { + if isDelim(r) { + startOfWord = true + result.WriteRune(r) + } else { + if startOfWord { + result.WriteRune(unicode.ToLower(r)) + startOfWord = false + } else { + result.WriteRune(r) + } + } } - r, _ := util.AbbreviateFull(s, left, right) - return r + + return result.String() +} + +// base64encode encodes a string to Base64. +func base64encode(s string) string { + return base64.StdEncoding.EncodeToString([]byte(s)) } -func initials(s string) string { - // Wrap this just to eliminate the var args, which templates don't do well. - return util.Initials(s) + +// base64decode decodes a Base64 encoded string. +func base64decode(s string) string { + bytes, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return err.Error() + } + return string(bytes) +} + +// base32encode encodes a string to Base32. +func base32encode(s string) string { + return base32.StdEncoding.EncodeToString([]byte(s)) +} + +// base32decode decodes a Base32 encoded string. +func base32decode(s string) string { + bytes, err := base32.StdEncoding.DecodeString(s) + if err != nil { + return err.Error() + } + return string(bytes) } func randAlphaNumeric(count int) string { - // It is not possible, it appears, to actually generate an error here. - r, _ := util.CryptoRandomAlphaNumeric(count) - return r + return cryptoRandomString(count, cryptoRandomStringOpts{letters: true, numbers: true}) } func randAlpha(count int) string { - r, _ := util.CryptoRandomAlphabetic(count) - return r + return cryptoRandomString(count, cryptoRandomStringOpts{letters: true}) } func randAscii(count int) string { - r, _ := util.CryptoRandomAscii(count) - return r + return cryptoRandomString(count, cryptoRandomStringOpts{ascii: true}) } func randNumeric(count int) string { - r, _ := util.CryptoRandomNumeric(count) - return r + return cryptoRandomString(count, cryptoRandomStringOpts{numbers: true}) } func untitle(str string) string { - return util.Uncapitalize(str) + return uncapitalize(str, "") } func quote(str ...interface{}) string { - out := make([]string, 0, len(str)) - for _, s := range str { - if s != nil { - out = append(out, fmt.Sprintf("%q", strval(s))) + var build strings.Builder + for i, s := range str { + if s == nil { + continue + } + if i > 0 { + build.WriteRune(' ') } + build.WriteString(fmt.Sprintf("%q", fmt.Sprint(s))) } - return strings.Join(out, " ") + return build.String() } func squote(str ...interface{}) string { - out := make([]string, 0, len(str)) - for _, s := range str { - if s != nil { - out = append(out, fmt.Sprintf("'%v'", s)) + var builder strings.Builder + for i, s := range str { + if s == nil { + continue } + if i > 0 { + builder.WriteRune(' ') + } + // Use fmt.Sprint to convert interface{} to string, then quote it. + builder.WriteRune('\'') + builder.WriteString(fmt.Sprint(s)) + builder.WriteRune('\'') } - return strings.Join(out, " ") + return builder.String() } +// Efficiently concatenates non-nil elements of v, separated by spaces. func cat(v ...interface{}) string { - v = removeNilElements(v) - r := strings.TrimSpace(strings.Repeat("%v ", len(v))) - return fmt.Sprintf(r, v...) + var builder strings.Builder + for i, item := range v { + if item == nil { + continue // Skip nil elements + } + if i > 0 { + builder.WriteRune(' ') // Add space between elements + } + // Append the string representation of the item + builder.WriteString(fmt.Sprint(item)) + } + // Return the concatenated string without trailing spaces + return builder.String() } +// Efficiently indents each line of the input string `v` with `spaces` number of spaces. func indent(spaces int, v string) string { + var builder strings.Builder pad := strings.Repeat(" ", spaces) - return pad + strings.Replace(v, "\n", "\n"+pad, -1) + lines := strings.Split(v, "\n") + + for i, line := range lines { + if i > 0 { + builder.WriteString("\n" + pad) + } else { + builder.WriteString(pad) + } + builder.WriteString(line) + } + + return builder.String() } +// Adds a newline at the start and then indents each line of `v` with `spaces` number of spaces. func nindent(spaces int, v string) string { return "\n" + indent(spaces, v) } @@ -126,49 +325,8 @@ func plural(one, many string, count int) string { return many } -func strslice(v interface{}) []string { - switch v := v.(type) { - case []string: - return v - case []interface{}: - b := make([]string, 0, len(v)) - for _, s := range v { - if s != nil { - b = append(b, strval(s)) - } - } - return b - default: - val := reflect.ValueOf(v) - switch val.Kind() { - case reflect.Array, reflect.Slice: - l := val.Len() - b := make([]string, 0, l) - for i := 0; i < l; i++ { - value := val.Index(i).Interface() - if value != nil { - b = append(b, strval(value)) - } - } - return b - default: - if v == nil { - return []string{} - } - - return []string{strval(v)} - } - } -} - -func removeNilElements(v []interface{}) []interface{} { - newSlice := make([]interface{}, 0, len(v)) - for _, i := range v { - if i != nil { - newSlice = append(newSlice, i) - } - } - return newSlice +func join(sep string, v interface{}) string { + return strings.Join(strslice(v), sep) } func strval(v interface{}) string { @@ -182,55 +340,102 @@ func strval(v interface{}) string { case fmt.Stringer: return v.String() default: + // Handles any other types by leveraging fmt.Sprintf for a string representation. return fmt.Sprintf("%v", v) } } +// strslice attempts to convert a variety of slice types to a slice of strings, optimizing performance and minimizing assignments. +func strslice(v interface{}) []string { + if v == nil { + return []string{} + } + + // Handle []string type efficiently without reflection. + if strs, ok := v.([]string); ok { + return strs + } + + // For slices of interface{}, convert each element to a string, skipping nil values. + if interfaces, ok := v.([]interface{}); ok { + var result []string + for _, s := range interfaces { + if s != nil { + result = append(result, strval(s)) + } + } + return result + } + + // Use reflection for other slice types to convert them to []string. + val := reflect.ValueOf(v) + if val.Kind() == reflect.Slice || val.Kind() == reflect.Array { + var result []string + for i := 0; i < val.Len(); i++ { + value := val.Index(i).Interface() + if value != nil { + result = append(result, strval(value)) + } + } + return result + } + + // If it's not a slice, array, or nil, return a slice with the string representation of v. + return []string{strval(v)} +} + func trunc(c int, s string) string { - if c < 0 && len(s)+c > 0 { - return s[len(s)+c:] + length := len(s) + + if c < 0 && length+c > 0 { + return s[length+c:] } - if c >= 0 && len(s) > c { + + if c >= 0 && length > c { return s[:c] } - return s -} -func join(sep string, v interface{}) string { - return strings.Join(strslice(v), sep) + return s } +// Splits `orig` string by `sep` and returns a map of the resulting parts. +// Each key is prefixed with an underscore followed by the part's index. func split(sep, orig string) map[string]string { parts := strings.Split(orig, sep) - res := make(map[string]string, len(parts)) - for i, v := range parts { - res["_"+strconv.Itoa(i)] = v - } - return res + return fillMapWithParts(parts) } +// Splits `orig` string by `sep` into at most `n` parts and returns a map of the parts. +// Each key is prefixed with an underscore followed by the part's index. func splitn(sep string, n int, orig string) map[string]string { parts := strings.SplitN(orig, sep, n) + return fillMapWithParts(parts) +} + +// fillMapWithParts fills a map with the provided parts, using a key format. +func fillMapWithParts(parts []string) map[string]string { res := make(map[string]string, len(parts)) for i, v := range parts { - res["_"+strconv.Itoa(i)] = v + res[fmt.Sprintf("_%d", i)] = v } return res } -// substring creates a substring of the given string. -// -// If start is < 0, this calls string[:end]. -// -// If start is >= 0 and end < 0 or end bigger than s length, this calls string[start:] -// -// Otherwise, this calls string[start, end]. func substring(start, end int, s string) string { if start < 0 { - return s[:end] + start = len(s) + start + } + if end < 0 { + end = len(s) + end + } + if start < 0 { + start = 0 + } + if end > len(s) { + end = len(s) } - if end < 0 || end > len(s) { - return s[start:] + if start > end { + return "" } return s[start:end] } diff --git a/strings_test.go b/strings_test.go index a75ab08..49eee4a 100644 --- a/strings_test.go +++ b/strings_test.go @@ -1,4 +1,4 @@ -package sprig +package sprout import ( "encoding/base32" diff --git a/url.go b/url.go index b8e120e..5a176c4 100644 --- a/url.go +++ b/url.go @@ -1,4 +1,4 @@ -package sprig +package sprout import ( "fmt" diff --git a/url_test.go b/url_test.go index f9c00b1..c091488 100644 --- a/url_test.go +++ b/url_test.go @@ -1,4 +1,4 @@ -package sprig +package sprout import ( "testing"