1
0
Fork 0
mirror of https://github.com/binwiederhier/ntfy.git synced 2025-12-02 05:10:06 +01:00
This commit is contained in:
binwiederhier 2025-07-21 11:24:58 +02:00
parent c807b5db21
commit 50c564d8a2
15 changed files with 1132 additions and 78 deletions

View file

@ -9,21 +9,38 @@ import (
"hash/adler32"
)
// sha512sum computes the SHA-512 hash of the input string and returns it as a hex-encoded string.
// This function can be used in templates to generate secure hashes of sensitive data.
//
// Example usage in templates: {{ "hello world" | sha512sum }}
func sha512sum(input string) string {
hash := sha512.Sum512([]byte(input))
return hex.EncodeToString(hash[:])
}
// sha256sum computes the SHA-256 hash of the input string and returns it as a hex-encoded string.
// This is a commonly used cryptographic hash function that produces a 256-bit (32-byte) hash value.
//
// Example usage in templates: {{ "hello world" | sha256sum }}
func sha256sum(input string) string {
hash := sha256.Sum256([]byte(input))
return hex.EncodeToString(hash[:])
}
// sha1sum computes the SHA-1 hash of the input string and returns it as a hex-encoded string.
// Note: SHA-1 is no longer considered secure against well-funded attackers for cryptographic purposes.
// Consider using sha256sum or sha512sum for security-critical applications.
//
// Example usage in templates: {{ "hello world" | sha1sum }}
func sha1sum(input string) string {
hash := sha1.Sum([]byte(input))
return hex.EncodeToString(hash[:])
}
// adler32sum computes the Adler-32 checksum of the input string and returns it as a decimal string.
// This is a non-cryptographic hash function primarily used for error detection.
//
// Example usage in templates: {{ "hello world" | adler32sum }}
func adler32sum(input string) string {
hash := adler32.Checksum([]byte(input))
return fmt.Sprintf("%d", hash)

View file

@ -1,27 +1,61 @@
package sprig
import (
"math"
"strconv"
"time"
)
// Given a format and a date, format the date string.
// date formats a date according to the provided format string.
//
// Date can be a `time.Time` or an `int, int32, int64`.
// In the later case, it is treated as seconds since UNIX
// epoch.
// Parameters:
// - fmt: A Go time format string (e.g., "2006-01-02 15:04:05")
// - date: Can be a time.Time, *time.Time, or int/int32/int64 (seconds since UNIX epoch)
//
// If date is not one of the recognized types, the current time is used.
//
// Example usage in templates: {{ now | date "2006-01-02" }}
func date(fmt string, date any) string {
return dateInZone(fmt, date, "Local")
}
// htmlDate formats a date in HTML5 date format (YYYY-MM-DD).
//
// Parameters:
// - date: Can be a time.Time, *time.Time, or int/int32/int64 (seconds since UNIX epoch)
//
// If date is not one of the recognized types, the current time is used.
//
// Example usage in templates: {{ now | htmlDate }}
func htmlDate(date any) string {
return dateInZone("2006-01-02", date, "Local")
}
// htmlDateInZone formats a date in HTML5 date format (YYYY-MM-DD) in the specified timezone.
//
// Parameters:
// - date: Can be a time.Time, *time.Time, or int/int32/int64 (seconds since UNIX epoch)
// - zone: Timezone name (e.g., "UTC", "America/New_York")
//
// If date is not one of the recognized types, the current time is used.
// If the timezone is invalid, UTC is used.
//
// Example usage in templates: {{ now | htmlDateInZone "UTC" }}
func htmlDateInZone(date any, zone string) string {
return dateInZone("2006-01-02", date, zone)
}
// dateInZone formats a date according to the provided format string in the specified timezone.
//
// Parameters:
// - fmt: A Go time format string (e.g., "2006-01-02 15:04:05")
// - date: Can be a time.Time, *time.Time, or int/int32/int64 (seconds since UNIX epoch)
// - zone: Timezone name (e.g., "UTC", "America/New_York")
//
// If date is not one of the recognized types, the current time is used.
// If the timezone is invalid, UTC is used.
//
// Example usage in templates: {{ now | dateInZone "2006-01-02 15:04:05" "UTC" }}
func dateInZone(fmt string, date any, zone string) string {
var t time.Time
switch date := date.(type) {
@ -45,6 +79,15 @@ func dateInZone(fmt string, date any, zone string) string {
return t.In(loc).Format(fmt)
}
// dateModify modifies a date by adding a duration and returns the resulting time.
//
// Parameters:
// - fmt: A duration string (e.g., "24h", "-12h30m", "1h15m30s")
// - date: The time.Time to modify
//
// If the duration string is invalid, the original date is returned.
//
// Example usage in templates: {{ now | dateModify "-24h" }}
func dateModify(fmt string, date time.Time) time.Time {
d, err := time.ParseDuration(fmt)
if err != nil {
@ -53,6 +96,15 @@ func dateModify(fmt string, date time.Time) time.Time {
return date.Add(d)
}
// mustDateModify modifies a date by adding a duration and returns the resulting time or an error.
//
// Parameters:
// - fmt: A duration string (e.g., "24h", "-12h30m", "1h15m30s")
// - date: The time.Time to modify
//
// Unlike dateModify, this function returns an error if the duration string is invalid.
//
// Example usage in templates: {{ now | mustDateModify "24h" }}
func mustDateModify(fmt string, date time.Time) (time.Time, error) {
d, err := time.ParseDuration(fmt)
if err != nil {
@ -61,6 +113,14 @@ func mustDateModify(fmt string, date time.Time) (time.Time, error) {
return date.Add(d), nil
}
// dateAgo returns a string representing the time elapsed since the given date.
//
// Parameters:
// - date: Can be a time.Time, int, or int64 (seconds since UNIX epoch)
//
// If date is not one of the recognized types, the current time is used.
//
// Example usage in templates: {{ "2023-01-01" | toDate "2006-01-02" | dateAgo }}
func dateAgo(date any) string {
var t time.Time
switch date := date.(type) {
@ -76,6 +136,12 @@ func dateAgo(date any) string {
return time.Since(t).Round(time.Second).String()
}
// duration converts seconds to a duration string.
//
// Parameters:
// - sec: Can be a string (parsed as int64), or int64 representing seconds
//
// Example usage in templates: {{ 3600 | duration }} -> "1h0m0s"
func duration(sec any) string {
var n int64
switch value := sec.(type) {
@ -89,6 +155,15 @@ func duration(sec any) string {
return (time.Duration(n) * time.Second).String()
}
// durationRound formats a duration in a human-readable rounded format.
//
// Parameters:
// - duration: Can be a string (parsed as duration), int64 (nanoseconds),
// or time.Time (time since that moment)
//
// Returns a string with the largest appropriate unit (y, mo, d, h, m, s).
//
// Example usage in templates: {{ 3600 | duration | durationRound }} -> "1h"
func durationRound(duration any) string {
var d time.Duration
switch duration := duration.(type) {
@ -101,10 +176,7 @@ func durationRound(duration any) string {
case time.Time:
d = time.Since(duration)
}
var u uint64
if d < 0 {
u = -u
}
u := uint64(math.Abs(float64(d)))
var (
year = uint64(time.Hour) * 24 * 365
month = uint64(time.Hour) * 24 * 30
@ -130,15 +202,39 @@ func durationRound(duration any) string {
return "0s"
}
// toDate parses a string into a time.Time using the specified format.
//
// Parameters:
// - fmt: A Go time format string (e.g., "2006-01-02")
// - str: The date string to parse
//
// If parsing fails, returns a zero time.Time.
//
// Example usage in templates: {{ "2023-01-01" | toDate "2006-01-02" }}
func toDate(fmt, str string) time.Time {
t, _ := time.ParseInLocation(fmt, str, time.Local)
return t
}
// mustToDate parses a string into a time.Time using the specified format or returns an error.
//
// Parameters:
// - fmt: A Go time format string (e.g., "2006-01-02")
// - str: The date string to parse
//
// Unlike toDate, this function returns an error if parsing fails.
//
// Example usage in templates: {{ mustToDate "2006-01-02" "2023-01-01" }}
func mustToDate(fmt, str string) (time.Time, error) {
return time.ParseInLocation(fmt, str, time.Local)
}
// unixEpoch returns the Unix timestamp (seconds since January 1, 1970 UTC) for the given time.
//
// Parameters:
// - date: A time.Time value
//
// Example usage in templates: {{ now | unixEpoch }}
func unixEpoch(date time.Time) string {
return strconv.FormatInt(date.Unix(), 10)
}

View file

@ -117,4 +117,7 @@ func TestDurationRound(t *testing.T) {
if err := runtv(tpl, "3mo", map[string]any{"Time": "2400h5s"}); err != nil {
t.Error(err)
}
if err := runtv(tpl, "1m", map[string]any{"Time": "-1m1s"}); err != nil {
t.Error(err)
}
}

View file

@ -25,6 +25,21 @@ func defaultValue(d any, given ...any) any {
}
// empty returns true if the given value has the zero value for its type.
// This is a helper function used by defaultValue, coalesce, all, and anyNonEmpty.
//
// The following values are considered empty:
// - Invalid values
// - nil values
// - Zero-length arrays, slices, maps, and strings
// - Boolean false
// - Zero for all numeric types
// - Structs are never considered empty
//
// Parameters:
// - given: The value to check for emptiness
//
// Returns:
// - bool: True if the value is considered empty, false otherwise
func empty(given any) bool {
g := reflect.ValueOf(given)
if !g.IsValid() {
@ -51,7 +66,16 @@ func empty(given any) bool {
}
}
// coalesce returns the first non-empty value.
// coalesce returns the first non-empty value from a list of values.
// If all values are empty, it returns nil.
//
// This is useful for providing a series of fallback values.
//
// Parameters:
// - v: A variadic list of values to check
//
// Returns:
// - any: The first non-empty value, or nil if all values are empty
func coalesce(v ...any) any {
for _, val := range v {
if !empty(val) {
@ -61,8 +85,15 @@ func coalesce(v ...any) any {
return nil
}
// all returns true if empty(x) is false for all values x in the list.
// If the list is empty, return true.
// all checks if all values in a list are non-empty.
// Returns true if every value in the list is non-empty.
// If the list is empty, returns true (vacuously true).
//
// Parameters:
// - v: A variadic list of values to check
//
// Returns:
// - bool: True if all values are non-empty, false otherwise
func all(v ...any) bool {
for _, val := range v {
if empty(val) {
@ -72,8 +103,15 @@ func all(v ...any) bool {
return true
}
// anyNonEmpty returns true if empty(x) is false for anyNonEmpty x in the list.
// If the list is empty, return false.
// anyNonEmpty checks if at least one value in a list is non-empty.
// Returns true if any value in the list is non-empty.
// If the list is empty, returns false.
//
// Parameters:
// - v: A variadic list of values to check
//
// Returns:
// - bool: True if at least one value is non-empty, false otherwise
func anyNonEmpty(v ...any) bool {
for _, val := range v {
if !empty(val) {
@ -83,25 +121,58 @@ func anyNonEmpty(v ...any) bool {
return false
}
// fromJSON decodes JSON into a structured value, ignoring errors.
// fromJSON decodes a JSON string into a structured value.
// This function ignores any errors that occur during decoding.
// If the JSON is invalid, it returns nil.
//
// Parameters:
// - v: The JSON string to decode
//
// Returns:
// - any: The decoded value, or nil if decoding failed
func fromJSON(v string) any {
output, _ := mustFromJSON(v)
return output
}
// mustFromJSON decodes JSON into a structured value, returning errors.
// mustFromJSON decodes a JSON string into a structured value.
// Unlike fromJSON, this function returns any errors that occur during decoding.
//
// Parameters:
// - v: The JSON string to decode
//
// Returns:
// - any: The decoded value
// - error: Any error that occurred during decoding
func mustFromJSON(v string) (any, error) {
var output any
err := json.Unmarshal([]byte(v), &output)
return output, err
}
// toJSON encodes an item into a JSON string
// toJSON encodes a value into a JSON string.
// This function ignores any errors that occur during encoding.
// If the value cannot be encoded, it returns an empty string.
//
// Parameters:
// - v: The value to encode to JSON
//
// Returns:
// - string: The JSON string representation of the value
func toJSON(v any) string {
output, _ := json.Marshal(v)
return string(output)
}
// mustToJSON encodes a value into a JSON string.
// Unlike toJSON, this function returns any errors that occur during encoding.
//
// Parameters:
// - v: The value to encode to JSON
//
// Returns:
// - string: The JSON string representation of the value
// - error: Any error that occurred during encoding
func mustToJSON(v any) (string, error) {
output, err := json.Marshal(v)
if err != nil {
@ -110,12 +181,29 @@ func mustToJSON(v any) (string, error) {
return string(output), nil
}
// toPrettyJSON encodes an item into a pretty (indented) JSON string
// toPrettyJSON encodes a value into a pretty (indented) JSON string.
// This function ignores any errors that occur during encoding.
// If the value cannot be encoded, it returns an empty string.
//
// Parameters:
// - v: The value to encode to JSON
//
// Returns:
// - string: The indented JSON string representation of the value
func toPrettyJSON(v any) string {
output, _ := json.MarshalIndent(v, "", " ")
return string(output)
}
// mustToPrettyJSON encodes a value into a pretty (indented) JSON string.
// Unlike toPrettyJSON, this function returns any errors that occur during encoding.
//
// Parameters:
// - v: The value to encode to JSON
//
// Returns:
// - string: The indented JSON string representation of the value
// - error: Any error that occurred during encoding
func mustToPrettyJSON(v any) (string, error) {
output, err := json.MarshalIndent(v, "", " ")
if err != nil {
@ -124,7 +212,15 @@ func mustToPrettyJSON(v any) (string, error) {
return string(output), nil
}
// toRawJSON encodes an item into a JSON string with no escaping of HTML characters.
// toRawJSON encodes a value into a JSON string with no escaping of HTML characters.
// This function panics if an error occurs during encoding.
// Unlike toJSON, HTML characters like <, >, and & are not escaped.
//
// Parameters:
// - v: The value to encode to JSON
//
// Returns:
// - string: The JSON string representation of the value without HTML escaping
func toRawJSON(v any) string {
output, err := mustToRawJSON(v)
if err != nil {
@ -133,7 +229,16 @@ func toRawJSON(v any) string {
return output
}
// mustToRawJSON encodes an item into a JSON string with no escaping of HTML characters.
// mustToRawJSON encodes a value into a JSON string with no escaping of HTML characters.
// Unlike toRawJSON, this function returns any errors that occur during encoding.
// HTML characters like <, >, and & are not escaped in the output.
//
// Parameters:
// - v: The value to encode to JSON
//
// Returns:
// - string: The JSON string representation of the value without HTML escaping
// - error: Any error that occurred during encoding
func mustToRawJSON(v any) (string, error) {
buf := new(bytes.Buffer)
enc := json.NewEncoder(buf)
@ -144,7 +249,17 @@ func mustToRawJSON(v any) (string, error) {
return strings.TrimSuffix(buf.String(), "\n"), nil
}
// ternary returns the first value if the last value is true, otherwise returns the second value.
// ternary implements a conditional (ternary) operator.
// It returns the first value if the condition is true, otherwise returns the second value.
// This is similar to the ?: operator in many programming languages.
//
// Parameters:
// - vt: The value to return if the condition is true
// - vf: The value to return if the condition is false
// - v: The boolean condition to evaluate
//
// Returns:
// - any: Either vt or vf depending on the value of v
func ternary(vt any, vf any, v bool) any {
if v {
return vt

View file

@ -1,5 +1,15 @@
package sprig
// get retrieves a value from a map by its key.
// If the key exists, returns the corresponding value.
// If the key doesn't exist, returns an empty string.
//
// Parameters:
// - d: The map to retrieve the value from
// - key: The key to look up
//
// Returns:
// - any: The value associated with the key, or an empty string if not found
func get(d map[string]any, key string) any {
if val, ok := d[key]; ok {
return val
@ -7,21 +17,58 @@ func get(d map[string]any, key string) any {
return ""
}
// set adds or updates a key-value pair in a map.
// Modifies the map in place and returns the modified map.
//
// Parameters:
// - d: The map to modify
// - key: The key to set
// - value: The value to associate with the key
//
// Returns:
// - map[string]any: The modified map (same instance as the input map)
func set(d map[string]any, key string, value any) map[string]any {
d[key] = value
return d
}
// unset removes a key-value pair from a map.
// If the key doesn't exist, the map remains unchanged.
// Modifies the map in place and returns the modified map.
//
// Parameters:
// - d: The map to modify
// - key: The key to remove
//
// Returns:
// - map[string]any: The modified map (same instance as the input map)
func unset(d map[string]any, key string) map[string]any {
delete(d, key)
return d
}
// hasKey checks if a key exists in a map.
//
// Parameters:
// - d: The map to check
// - key: The key to look for
//
// Returns:
// - bool: True if the key exists in the map, false otherwise
func hasKey(d map[string]any, key string) bool {
_, ok := d[key]
return ok
}
// pluck extracts values for a specific key from multiple maps.
// Only includes values from maps where the key exists.
//
// Parameters:
// - key: The key to extract values for
// - d: A variadic list of maps to extract values from
//
// Returns:
// - []any: A slice containing all values associated with the key across all maps
func pluck(key string, d ...map[string]any) []any {
var res []any
for _, dict := range d {
@ -32,6 +79,14 @@ func pluck(key string, d ...map[string]any) []any {
return res
}
// keys collects all keys from one or more maps.
// The returned slice may contain duplicate keys if multiple maps contain the same key.
//
// Parameters:
// - dicts: A variadic list of maps to collect keys from
//
// Returns:
// - []string: A slice containing all keys from all provided maps
func keys(dicts ...map[string]any) []string {
var k []string
for _, dict := range dicts {
@ -42,6 +97,15 @@ func keys(dicts ...map[string]any) []string {
return k
}
// pick creates a new map containing only the specified keys from the original map.
// If a key doesn't exist in the original map, it won't be included in the result.
//
// Parameters:
// - dict: The source map
// - keys: A variadic list of keys to include in the result
//
// Returns:
// - map[string]any: A new map containing only the specified keys and their values
func pick(dict map[string]any, keys ...string) map[string]any {
res := map[string]any{}
for _, k := range keys {
@ -52,6 +116,15 @@ func pick(dict map[string]any, keys ...string) map[string]any {
return res
}
// omit creates a new map excluding the specified keys from the original map.
// The original map remains unchanged.
//
// Parameters:
// - dict: The source map
// - keys: A variadic list of keys to exclude from the result
//
// Returns:
// - map[string]any: A new map containing all key-value pairs except those specified
func omit(dict map[string]any, keys ...string) map[string]any {
res := map[string]any{}
omit := make(map[string]bool, len(keys))
@ -66,6 +139,16 @@ func omit(dict map[string]any, keys ...string) map[string]any {
return res
}
// dict creates a new map from a list of key-value pairs.
// The arguments are treated as key-value pairs, where even-indexed arguments are keys
// and odd-indexed arguments are values.
// If there's an odd number of arguments, the last key will be assigned an empty string value.
//
// Parameters:
// - v: A variadic list of alternating keys and values
//
// Returns:
// - map[string]any: A new map containing the specified key-value pairs
func dict(v ...any) map[string]any {
dict := map[string]any{}
lenv := len(v)
@ -80,6 +163,14 @@ func dict(v ...any) map[string]any {
return dict
}
// values collects all values from a map into a slice.
// The order of values in the resulting slice is not guaranteed.
//
// Parameters:
// - dict: The map to collect values from
//
// Returns:
// - []any: A slice containing all values from the map
func values(dict map[string]any) []any {
var values []any
for _, value := range dict {
@ -88,6 +179,22 @@ func values(dict map[string]any) []any {
return values
}
// dig safely accesses nested values in maps using a sequence of keys.
// If any key in the path doesn't exist, it returns the default value.
// The function expects at least 3 arguments: one or more keys, a default value, and a map.
//
// Parameters:
// - ps: A variadic list where:
// - The first N-2 arguments are string keys forming the path
// - The second-to-last argument is the default value to return if the path doesn't exist
// - The last argument is the map to traverse
//
// Returns:
// - any: The value found at the specified path, or the default value if not found
// - error: Any error that occurred during traversal
//
// Panics:
// - If fewer than 3 arguments are provided
func dig(ps ...any) (any, error) {
if len(ps) < 3 {
panic("dig needs at least three arguments")
@ -102,6 +209,17 @@ func dig(ps ...any) (any, error) {
return digFromDict(dict, def, ks)
}
// digFromDict is a helper function for dig that recursively traverses a map using a sequence of keys.
// If any key in the path doesn't exist, it returns the default value.
//
// Parameters:
// - dict: The map to traverse
// - d: The default value to return if the path doesn't exist
// - ks: A slice of string keys forming the path to traverse
//
// Returns:
// - any: The value found at the specified path, or the default value if not found
// - error: Any error that occurred during traversal
func digFromDict(dict map[string]any, d any, ks []string) (any, error) {
k, ns := ks[0], ks[1:]
step, has := dict[k]

View file

@ -2,6 +2,7 @@ package sprig
import "errors"
// fail is a function that always returns an error with the given message.
func fail(msg string) (string, error) {
return "", errors.New(msg)
}

View file

@ -12,6 +12,7 @@ import (
const (
loopExecutionLimit = 10_000 // Limit the number of loop executions to prevent execution from taking too long
stringLengthLimit = 100_000 // Limit the length of strings to prevent memory issues
sliceSizeLimit = 10_000 // Limit the size of slices to prevent memory issues
)
// TxtFuncMap produces the function map.

View file

@ -52,7 +52,7 @@ func runtv(tpl, expect string, vars any) error {
return err
}
if expect != b.String() {
return fmt.Errorf("Expected '%s', got '%s'", expect, b.String())
return fmt.Errorf("expected '%s', got '%s'", expect, b.String())
}
return nil
}

View file

@ -11,10 +11,15 @@ import (
// ints, and other types not implementing []any can be worked with.
// For example, this is useful if you need to work on the output of regexs.
// list creates a new list (slice) containing the provided arguments.
// It accepts any number of arguments of any type and returns them as a slice.
func list(v ...any) []any {
return v
}
// push appends an element to the end of a list (slice or array).
// It takes a list and a value, and returns a new list with the value appended.
// This function will panic if the first argument is not a slice or array.
func push(list any, v any) []any {
l, err := mustPush(list, v)
if err != nil {
@ -23,99 +28,103 @@ func push(list any, v any) []any {
return l
}
// mustPush is the implementation of push that returns an error instead of panicking.
// It converts the input list to a slice of any type, then appends the value.
func mustPush(list any, v any) ([]any, error) {
tp := reflect.TypeOf(list).Kind()
switch tp {
case reflect.Slice, reflect.Array:
l2 := reflect.ValueOf(list)
l := l2.Len()
nl := make([]any, l)
for i := 0; i < l; i++ {
nl[i] = l2.Index(i).Interface()
}
return append(nl, v), nil
default:
return nil, fmt.Errorf("cannot push on type %s", tp)
}
}
// prepend adds an element to the beginning of a list (slice or array).
// It takes a list and a value, and returns a new list with the value at the start.
// This function will panic if the first argument is not a slice or array.
func prepend(list any, v any) []any {
l, err := mustPrepend(list, v)
if err != nil {
panic(err)
}
return l
}
// mustPrepend is the implementation of prepend that returns an error instead of panicking.
// It converts the input list to a slice of any type, then prepends the value.
func mustPrepend(list any, v any) ([]any, error) {
//return append([]any{v}, list...)
tp := reflect.TypeOf(list).Kind()
switch tp {
case reflect.Slice, reflect.Array:
l2 := reflect.ValueOf(list)
l := l2.Len()
nl := make([]any, l)
for i := 0; i < l; i++ {
nl[i] = l2.Index(i).Interface()
}
return append([]any{v}, nl...), nil
default:
return nil, fmt.Errorf("cannot prepend on type %s", tp)
}
}
// chunk divides a list into sub-lists of the specified size.
// It takes a size and a list, and returns a list of lists, each containing
// up to 'size' elements from the original list.
// This function will panic if the second argument is not a slice or array.
func chunk(size int, list any) [][]any {
l, err := mustChunk(size, list)
if err != nil {
panic(err)
}
return l
}
// mustChunk is the implementation of chunk that returns an error instead of panicking.
// It divides the input list into chunks of the specified size.
func mustChunk(size int, list any) ([][]any, error) {
tp := reflect.TypeOf(list).Kind()
switch tp {
case reflect.Slice, reflect.Array:
l2 := reflect.ValueOf(list)
l := l2.Len()
cs := int(math.Floor(float64(l-1)/float64(size)) + 1)
nl := make([][]any, cs)
for i := 0; i < cs; i++ {
numChunks := int(math.Floor(float64(l-1)/float64(size)) + 1)
if numChunks > sliceSizeLimit {
return nil, fmt.Errorf("number of chunks %d exceeds maximum limit of %d", numChunks, sliceSizeLimit)
}
result := make([][]any, numChunks)
for i := 0; i < numChunks; i++ {
clen := size
if i == cs-1 {
// Handle the last chunk which might be smaller
if i == numChunks-1 {
clen = int(math.Floor(math.Mod(float64(l), float64(size))))
if clen == 0 {
clen = size
}
}
nl[i] = make([]any, clen)
result[i] = make([]any, clen)
for j := 0; j < clen; j++ {
ix := i*size + j
nl[i][j] = l2.Index(ix).Interface()
result[i][j] = l2.Index(ix).Interface()
}
}
return nl, nil
return result, nil
default:
return nil, fmt.Errorf("cannot chunk type %s", tp)
}
}
// last returns the last element of a list (slice or array).
// If the list is empty, it returns nil.
// This function will panic if the argument is not a slice or array.
func last(list any) any {
l, err := mustLast(list)
if err != nil {
@ -125,6 +134,8 @@ func last(list any) any {
return l
}
// mustLast is the implementation of last that returns an error instead of panicking.
// It returns the last element of the list or nil if the list is empty.
func mustLast(list any) (any, error) {
tp := reflect.TypeOf(list).Kind()
switch tp {
@ -142,6 +153,9 @@ func mustLast(list any) (any, error) {
}
}
// first returns the first element of a list (slice or array).
// If the list is empty, it returns nil.
// This function will panic if the argument is not a slice or array.
func first(list any) any {
l, err := mustFirst(list)
if err != nil {
@ -151,6 +165,8 @@ func first(list any) any {
return l
}
// mustFirst is the implementation of first that returns an error instead of panicking.
// It returns the first element of the list or nil if the list is empty.
func mustFirst(list any) (any, error) {
tp := reflect.TypeOf(list).Kind()
switch tp {
@ -168,6 +184,9 @@ func mustFirst(list any) (any, error) {
}
}
// rest returns all elements of a list except the first one.
// If the list is empty, it returns nil.
// This function will panic if the argument is not a slice or array.
func rest(list any) []any {
l, err := mustRest(list)
if err != nil {
@ -177,28 +196,30 @@ func rest(list any) []any {
return l
}
// mustRest is the implementation of rest that returns an error instead of panicking.
// It returns all elements of the list except the first one, or nil if the list is empty.
func mustRest(list any) ([]any, error) {
tp := reflect.TypeOf(list).Kind()
switch tp {
case reflect.Slice, reflect.Array:
l2 := reflect.ValueOf(list)
l := l2.Len()
if l == 0 {
return nil, nil
}
nl := make([]any, l-1)
for i := 1; i < l; i++ {
nl[i-1] = l2.Index(i).Interface()
}
return nl, nil
default:
return nil, fmt.Errorf("cannot find rest on type %s", tp)
}
}
// initial returns all elements of a list except the last one.
// If the list is empty, it returns nil.
// This function will panic if the argument is not a slice or array.
func initial(list any) []any {
l, err := mustInitial(list)
if err != nil {
@ -208,28 +229,30 @@ func initial(list any) []any {
return l
}
// mustInitial is the implementation of initial that returns an error instead of panicking.
// It returns all elements of the list except the last one, or nil if the list is empty.
func mustInitial(list any) ([]any, error) {
tp := reflect.TypeOf(list).Kind()
switch tp {
case reflect.Slice, reflect.Array:
l2 := reflect.ValueOf(list)
l := l2.Len()
if l == 0 {
return nil, nil
}
nl := make([]any, l-1)
for i := 0; i < l-1; i++ {
nl[i] = l2.Index(i).Interface()
}
return nl, nil
default:
return nil, fmt.Errorf("cannot find initial on type %s", tp)
}
}
// sortAlpha sorts a list of strings alphabetically.
// If the input is not a slice or array, it returns a single-element slice
// containing the string representation of the input.
func sortAlpha(list any) []string {
k := reflect.Indirect(reflect.ValueOf(list)).Kind()
switch k {
@ -242,6 +265,8 @@ func sortAlpha(list any) []string {
return []string{strval(list)}
}
// reverse returns a new list with the elements in reverse order.
// This function will panic if the argument is not a slice or array.
func reverse(v any) []any {
l, err := mustReverse(v)
if err != nil {
@ -251,42 +276,45 @@ func reverse(v any) []any {
return l
}
// mustReverse is the implementation of reverse that returns an error instead of panicking.
// It returns a new list with the elements in reverse order.
func mustReverse(v any) ([]any, error) {
tp := reflect.TypeOf(v).Kind()
switch tp {
case reflect.Slice, reflect.Array:
l2 := reflect.ValueOf(v)
l := l2.Len()
// We do not sort in place because the incoming array should not be altered.
nl := make([]any, l)
for i := 0; i < l; i++ {
nl[l-i-1] = l2.Index(i).Interface()
}
return nl, nil
default:
return nil, fmt.Errorf("cannot find reverse on type %s", tp)
}
}
// compact returns a new list with all "empty" elements removed.
// An element is considered empty if it's nil, zero, an empty string, or an empty collection.
// This function will panic if the argument is not a slice or array.
func compact(list any) []any {
l, err := mustCompact(list)
if err != nil {
panic(err)
}
return l
}
// mustCompact is the implementation of compact that returns an error instead of panicking.
// It returns a new list with all "empty" elements removed.
func mustCompact(list any) ([]any, error) {
tp := reflect.TypeOf(list).Kind()
switch tp {
case reflect.Slice, reflect.Array:
l2 := reflect.ValueOf(list)
l := l2.Len()
nl := []any{}
var nl []any
var item any
for i := 0; i < l; i++ {
item = l2.Index(i).Interface()
@ -294,30 +322,32 @@ func mustCompact(list any) ([]any, error) {
nl = append(nl, item)
}
}
return nl, nil
default:
return nil, fmt.Errorf("cannot compact on type %s", tp)
}
}
// uniq returns a new list with duplicate elements removed.
// The first occurrence of each element is kept.
// This function will panic if the argument is not a slice or array.
func uniq(list any) []any {
l, err := mustUniq(list)
if err != nil {
panic(err)
}
return l
}
// mustUniq is the implementation of uniq that returns an error instead of panicking.
// It returns a new list with duplicate elements removed.
func mustUniq(list any) ([]any, error) {
tp := reflect.TypeOf(list).Kind()
switch tp {
case reflect.Slice, reflect.Array:
l2 := reflect.ValueOf(list)
l := l2.Len()
dest := []any{}
var dest []any
var item any
for i := 0; i < l; i++ {
item = l2.Index(i).Interface()
@ -325,13 +355,15 @@ func mustUniq(list any) ([]any, error) {
dest = append(dest, item)
}
}
return dest, nil
default:
return nil, fmt.Errorf("cannot find uniq on type %s", tp)
}
}
// inList checks if a value is present in a list.
// It uses deep equality comparison to check for matches.
// Returns true if the value is found, false otherwise.
func inList(haystack []any, needle any) bool {
for _, h := range haystack {
if reflect.DeepEqual(needle, h) {
@ -341,21 +373,23 @@ func inList(haystack []any, needle any) bool {
return false
}
// without returns a new list with all occurrences of the specified values removed.
// This function will panic if the first argument is not a slice or array.
func without(list any, omit ...any) []any {
l, err := mustWithout(list, omit...)
if err != nil {
panic(err)
}
return l
}
// mustWithout is the implementation of without that returns an error instead of panicking.
// It returns a new list with all occurrences of the specified values removed.
func mustWithout(list any, omit ...any) ([]any, error) {
tp := reflect.TypeOf(list).Kind()
switch tp {
case reflect.Slice, reflect.Array:
l2 := reflect.ValueOf(list)
l := l2.Len()
res := []any{}
var item any
@ -365,22 +399,25 @@ func mustWithout(list any, omit ...any) ([]any, error) {
res = append(res, item)
}
}
return res, nil
default:
return nil, fmt.Errorf("cannot find without on type %s", tp)
}
}
// has checks if a value is present in a list.
// Returns true if the value is found, false otherwise.
// This function will panic if the second argument is not a slice or array.
func has(needle any, haystack any) bool {
l, err := mustHas(needle, haystack)
if err != nil {
panic(err)
}
return l
}
// mustHas is the implementation of has that returns an error instead of panicking.
// It checks if a value is present in a list.
func mustHas(needle any, haystack any) (bool, error) {
if haystack == nil {
return false, nil
@ -397,38 +434,41 @@ func mustHas(needle any, haystack any) (bool, error) {
return true, nil
}
}
return false, nil
default:
return false, fmt.Errorf("cannot find has on type %s", tp)
}
}
// slice extracts a portion of a list based on the provided indices.
// Usage examples:
// $list := [1, 2, 3, 4, 5]
// slice $list -> list[0:5] = list[:]
// slice $list 0 3 -> list[0:3] = list[:3]
// slice $list 3 5 -> list[3:5]
// slice $list 3 -> list[3:5] = list[3:]
//
// This function will panic if the first argument is not a slice or array.
func slice(list any, indices ...any) any {
l, err := mustSlice(list, indices...)
if err != nil {
panic(err)
}
return l
}
// mustSlice is the implementation of slice that returns an error instead of panicking.
// It extracts a portion of a list based on the provided indices.
func mustSlice(list any, indices ...any) (any, error) {
tp := reflect.TypeOf(list).Kind()
switch tp {
case reflect.Slice, reflect.Array:
l2 := reflect.ValueOf(list)
l := l2.Len()
if l == 0 {
return nil, nil
}
// Determine start and end indices
var start, end int
if len(indices) > 0 {
start = toInt(indices[0])
@ -438,13 +478,15 @@ func mustSlice(list any, indices ...any) (any, error) {
} else {
end = toInt(indices[1])
}
return l2.Slice(start, end).Interface(), nil
default:
return nil, fmt.Errorf("list should be type of slice or array but %s", tp)
}
}
// concat combines multiple lists into a single list.
// It takes any number of lists and returns a new list containing all elements.
// This function will panic if any argument is not a slice or array.
func concat(lists ...any) any {
var res []any
for _, list := range lists {

View file

@ -1,6 +1,7 @@
package sprig
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
@ -68,6 +69,8 @@ func TestMustChunk(t *testing.T) {
for tpl, expect := range tests {
assert.NoError(t, runt(tpl, expect))
}
err := runt(`{{ tuple `+strings.Repeat(" 0", 10001)+` | mustChunk 1 }}`, "a")
assert.ErrorContains(t, err, "number of chunks 10001 exceeds maximum limit of 10000")
}
func TestPrepend(t *testing.T) {

View file

@ -9,7 +9,20 @@ import (
"strings"
)
// toFloat64 converts 64-bit floats
// toFloat64 converts a value to a 64-bit float.
// It handles various input types:
// - string: parsed as a float, returns 0 if parsing fails
// - integer types: converted to float64
// - unsigned integer types: converted to float64
// - float types: returned as is
// - bool: true becomes 1.0, false becomes 0.0
// - other types: returns 0.0
//
// Parameters:
// - v: The value to convert to float64
//
// Returns:
// - float64: The converted value
func toFloat64(v any) float64 {
if str, ok := v.(string); ok {
iv, err := strconv.ParseFloat(str, 64)
@ -39,12 +52,27 @@ func toFloat64(v any) float64 {
}
}
// toInt converts a value to a 32-bit integer.
// This is a wrapper around toInt64 that casts the result to int.
//
// Parameters:
// - v: The value to convert to int
//
// Returns:
// - int: The converted value
func toInt(v any) int {
// It's not optimal. But I don't want duplicate toInt64 code.
return int(toInt64(v))
}
// toInt64 converts integer types to 64-bit integers
// toInt64 converts a value to a 64-bit integer.
// It handles various input types:
// - string: parsed as an integer, returns 0 if parsing fails
// - integer types: converted to int64
// - unsigned integer types: converted to int64 (values > MaxInt64 become MaxInt64)
// - float types: truncated to int64
// - bool: true becomes 1, false becomes 0
// - other types: returns 0
func toInt64(v any) int64 {
if str, ok := v.(string); ok {
iv, err := strconv.ParseInt(str, 10, 64)
@ -53,7 +81,6 @@ func toInt64(v any) int64 {
}
return iv
}
val := reflect.Indirect(reflect.ValueOf(v))
switch val.Kind() {
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
@ -79,10 +106,26 @@ func toInt64(v any) int64 {
}
}
// add1 increments a value by 1.
// The input is first converted to int64 using toInt64.
//
// Parameters:
// - i: The value to increment
//
// Returns:
// - int64: The incremented value
func add1(i any) int64 {
return toInt64(i) + 1
}
// add sums all the provided values.
// All inputs are converted to int64 using toInt64 before addition.
//
// Parameters:
// - i: A variadic list of values to sum
//
// Returns:
// - int64: The sum of all values
func add(i ...any) int64 {
var a int64
for _, b := range i {
@ -91,18 +134,61 @@ func add(i ...any) int64 {
return a
}
// sub subtracts the second value from the first.
// Both inputs are converted to int64 using toInt64 before subtraction.
//
// Parameters:
// - a: The value to subtract from
// - b: The value to subtract
//
// Returns:
// - int64: The result of a - b
func sub(a, b any) int64 {
return toInt64(a) - toInt64(b)
}
// div divides the first value by the second.
// Both inputs are converted to int64 using toInt64 before division.
// Note: This performs integer division, so the result is truncated.
//
// Parameters:
// - a: The dividend
// - b: The divisor
//
// Returns:
// - int64: The result of a / b
//
// Panics:
// - If b evaluates to 0 (division by zero)
func div(a, b any) int64 {
return toInt64(a) / toInt64(b)
}
// mod returns the remainder of dividing the first value by the second.
// Both inputs are converted to int64 using toInt64 before the modulo operation.
//
// Parameters:
// - a: The dividend
// - b: The divisor
//
// Returns:
// - int64: The remainder of a / b
//
// Panics:
// - If b evaluates to 0 (modulo by zero)
func mod(a, b any) int64 {
return toInt64(a) % toInt64(b)
}
// mul multiplies all the provided values.
// All inputs are converted to int64 using toInt64 before multiplication.
//
// Parameters:
// - a: The first value to multiply
// - v: Additional values to multiply with a
//
// Returns:
// - int64: The product of all values
func mul(a any, v ...any) int64 {
val := toInt64(a)
for _, b := range v {
@ -111,10 +197,30 @@ func mul(a any, v ...any) int64 {
return val
}
// randInt generates a random integer between min (inclusive) and max (exclusive).
//
// Parameters:
// - min: The lower bound (inclusive)
// - max: The upper bound (exclusive)
//
// Returns:
// - int: A random integer in the range [min, max)
//
// Panics:
// - If max <= min (via rand.Intn)
func randInt(min, max int) int {
return rand.Intn(max-min) + min
}
// maxAsInt64 returns the maximum value from a list of values as an int64.
// All inputs are converted to int64 using toInt64 before comparison.
//
// Parameters:
// - a: The first value to compare
// - i: Additional values to compare
//
// Returns:
// - int64: The maximum value from all inputs
func maxAsInt64(a any, i ...any) int64 {
aa := toInt64(a)
for _, b := range i {
@ -126,6 +232,15 @@ func maxAsInt64(a any, i ...any) int64 {
return aa
}
// maxAsFloat64 returns the maximum value from a list of values as a float64.
// All inputs are converted to float64 using toFloat64 before comparison.
//
// Parameters:
// - a: The first value to compare
// - i: Additional values to compare
//
// Returns:
// - float64: The maximum value from all inputs
func maxAsFloat64(a any, i ...any) float64 {
m := toFloat64(a)
for _, b := range i {
@ -134,6 +249,15 @@ func maxAsFloat64(a any, i ...any) float64 {
return m
}
// minAsInt64 returns the minimum value from a list of values as an int64.
// All inputs are converted to int64 using toInt64 before comparison.
//
// Parameters:
// - a: The first value to compare
// - i: Additional values to compare
//
// Returns:
// - int64: The minimum value from all inputs
func minAsInt64(a any, i ...any) int64 {
aa := toInt64(a)
for _, b := range i {
@ -145,6 +269,15 @@ func minAsInt64(a any, i ...any) int64 {
return aa
}
// minAsFloat64 returns the minimum value from a list of values as a float64.
// All inputs are converted to float64 using toFloat64 before comparison.
//
// Parameters:
// - a: The first value to compare
// - i: Additional values to compare
//
// Returns:
// - float64: The minimum value from all inputs
func minAsFloat64(a any, i ...any) float64 {
m := toFloat64(a)
for _, b := range i {
@ -153,6 +286,14 @@ func minAsFloat64(a any, i ...any) float64 {
return m
}
// until generates a sequence of integers from 0 to count (exclusive).
// If count is negative, it generates a sequence from 0 to count (inclusive) with step -1.
//
// Parameters:
// - count: The end value (exclusive if positive, inclusive if negative)
//
// Returns:
// - []int: A slice containing the generated sequence
func until(count int) []int {
step := 1
if count < 0 {
@ -161,6 +302,23 @@ func until(count int) []int {
return untilStep(0, count, step)
}
// untilStep generates a sequence of integers from start to stop with the specified step.
// The sequence is generated as follows:
// - If step is 0, returns an empty slice
// - If stop < start and step < 0, generates a decreasing sequence from start to stop (exclusive)
// - If stop > start and step > 0, generates an increasing sequence from start to stop (exclusive)
// - Otherwise, returns an empty slice
//
// Parameters:
// - start: The starting value (inclusive)
// - stop: The ending value (exclusive)
// - step: The increment between values
//
// Returns:
// - []int: A slice containing the generated sequence
//
// Panics:
// - If the number of iterations would exceed loopExecutionLimit
func untilStep(start, stop, step int) []int {
var v []int
if step == 0 {
@ -188,14 +346,44 @@ func untilStep(start, stop, step int) []int {
return v
}
// floor returns the greatest integer value less than or equal to the input.
// The input is first converted to float64 using toFloat64.
//
// Parameters:
// - a: The value to floor
//
// Returns:
// - float64: The greatest integer value less than or equal to a
func floor(a any) float64 {
return math.Floor(toFloat64(a))
}
// ceil returns the least integer value greater than or equal to the input.
// The input is first converted to float64 using toFloat64.
//
// Parameters:
// - a: The value to ceil
//
// Returns:
// - float64: The least integer value greater than or equal to a
func ceil(a any) float64 {
return math.Ceil(toFloat64(a))
}
// round rounds a number to a specified number of decimal places.
// The input is first converted to float64 using toFloat64.
//
// Parameters:
// - a: The value to round
// - p: The number of decimal places to round to
// - rOpt: Optional rounding threshold (default is 0.5)
//
// Returns:
// - float64: The rounded value
//
// Examples:
// - round(3.14159, 2) returns 3.14
// - round(3.14159, 2, 0.6) returns 3.14 (only rounds up if fraction ≥ 0.6)
func round(a any, p int, rOpt ...float64) float64 {
roundOn := .5
if len(rOpt) > 0 {
@ -203,7 +391,6 @@ func round(a any, p int, rOpt ...float64) float64 {
}
val := toFloat64(a)
places := toFloat64(p)
var round float64
pow := math.Pow(10, places)
digit := pow * val
@ -216,7 +403,15 @@ func round(a any, p int, rOpt ...float64) float64 {
return round / pow
}
// converts unix octal to decimal
// toDecimal converts a value from octal to decimal.
// The input is first converted to a string using fmt.Sprint, then parsed as an octal number.
// If the parsing fails, it returns 0.
//
// Parameters:
// - v: The octal value to convert
//
// Returns:
// - int64: The decimal representation of the octal value
func toDecimal(v any) int64 {
result, err := strconv.ParseInt(fmt.Sprint(v), 8, 64)
if err != nil {
@ -225,11 +420,34 @@ func toDecimal(v any) int64 {
return result
}
// atoi converts a string to an integer.
// If the conversion fails, it returns 0.
//
// Parameters:
// - a: The string to convert
//
// Returns:
// - int: The integer value of the string
func atoi(a string) int {
i, _ := strconv.Atoi(a)
return i
}
// seq generates a sequence of integers and returns them as a space-delimited string.
// The behavior depends on the number of parameters:
// - 0 params: Returns an empty string
// - 1 param: Generates sequence from 1 to param[0]
// - 2 params: Generates sequence from param[0] to param[1]
// - 3 params: Generates sequence from param[0] to param[2] with step param[1]
//
// If the end is less than the start, the sequence will be decreasing unless
// a positive step is explicitly provided (which would result in an empty string).
//
// Parameters:
// - params: Variable number of integers defining the sequence
//
// Returns:
// - string: A space-delimited string of the generated sequence
func seq(params ...int) string {
increment := 1
switch len(params) {
@ -266,6 +484,16 @@ func seq(params ...int) string {
}
}
// intArrayToString converts a slice of integers to a space-delimited string.
// The function removes the square brackets that would normally appear when
// converting a slice to a string.
//
// Parameters:
// - slice: The slice of integers to convert
// - delimiter: The delimiter to use between elements
//
// Returns:
// - string: A delimited string representation of the integer slice
func intArrayToString(slice []int, delimiter string) string {
return strings.Trim(strings.Join(strings.Fields(fmt.Sprint(slice)), delimiter), "[]")
}

View file

@ -6,23 +6,65 @@ import (
)
// typeIs returns true if the src is the type named in target.
// It compares the type name of src with the target string.
//
// Parameters:
// - target: The type name to check against
// - src: The value whose type will be checked
//
// Returns:
// - bool: True if the type name of src matches target, false otherwise
func typeIs(target string, src any) bool {
return target == typeOf(src)
}
// typeIsLike returns true if the src is the type named in target or a pointer to that type.
// This is useful when you need to check for both a type and a pointer to that type.
//
// Parameters:
// - target: The type name to check against
// - src: The value whose type will be checked
//
// Returns:
// - bool: True if the type of src matches target or "*"+target, false otherwise
func typeIsLike(target string, src any) bool {
t := typeOf(src)
return target == t || "*"+target == t
}
// typeOf returns the type of a value as a string.
// It uses fmt.Sprintf with the %T format verb to get the type name.
//
// Parameters:
// - src: The value whose type name will be returned
//
// Returns:
// - string: The type name of src
func typeOf(src any) string {
return fmt.Sprintf("%T", src)
}
// kindIs returns true if the kind of src matches the target kind.
// This checks the underlying kind (e.g., "string", "int", "map") rather than the specific type.
//
// Parameters:
// - target: The kind name to check against
// - src: The value whose kind will be checked
//
// Returns:
// - bool: True if the kind of src matches target, false otherwise
func kindIs(target string, src any) bool {
return target == kindOf(src)
}
// kindOf returns the kind of a value as a string.
// The kind represents the specific Go type category (e.g., "string", "int", "map", "slice").
//
// Parameters:
// - src: The value whose kind will be returned
//
// Returns:
// - string: The kind of src as a string
func kindOf(src any) string {
return reflect.ValueOf(src).Kind().String()
}

View file

@ -4,20 +4,60 @@ import (
"regexp"
)
// regexMatch checks if a string matches a regular expression pattern.
// It ignores any errors that might occur during regex compilation.
//
// Parameters:
// - regex: The regular expression pattern to match against
// - s: The string to check
//
// Returns:
// - bool: True if the string matches the pattern, false otherwise
func regexMatch(regex string, s string) bool {
match, _ := regexp.MatchString(regex, s)
return match
}
// mustRegexMatch checks if a string matches a regular expression pattern.
// Unlike regexMatch, this function returns any errors that occur during regex compilation.
//
// Parameters:
// - regex: The regular expression pattern to match against
// - s: The string to check
//
// Returns:
// - bool: True if the string matches the pattern, false otherwise
// - error: Any error that occurred during regex compilation
func mustRegexMatch(regex string, s string) (bool, error) {
return regexp.MatchString(regex, s)
}
// regexFindAll finds all matches of a regular expression in a string.
// It panics if the regex pattern cannot be compiled.
//
// Parameters:
// - regex: The regular expression pattern to search for
// - s: The string to search within
// - n: The maximum number of matches to return (negative means all matches)
//
// Returns:
// - []string: A slice containing all matched substrings
func regexFindAll(regex string, s string, n int) []string {
r := regexp.MustCompile(regex)
return r.FindAllString(s, n)
}
// mustRegexFindAll finds all matches of a regular expression in a string.
// Unlike regexFindAll, this function returns any errors that occur during regex compilation.
//
// Parameters:
// - regex: The regular expression pattern to search for
// - s: The string to search within
// - n: The maximum number of matches to return (negative means all matches)
//
// Returns:
// - []string: A slice containing all matched substrings
// - error: Any error that occurred during regex compilation
func mustRegexFindAll(regex string, s string, n int) ([]string, error) {
r, err := regexp.Compile(regex)
if err != nil {
@ -26,11 +66,30 @@ func mustRegexFindAll(regex string, s string, n int) ([]string, error) {
return r.FindAllString(s, n), nil
}
// regexFind finds the first match of a regular expression in a string.
// It panics if the regex pattern cannot be compiled.
//
// Parameters:
// - regex: The regular expression pattern to search for
// - s: The string to search within
//
// Returns:
// - string: The first matched substring, or an empty string if no match
func regexFind(regex string, s string) string {
r := regexp.MustCompile(regex)
return r.FindString(s)
}
// mustRegexFind finds the first match of a regular expression in a string.
// Unlike regexFind, this function returns any errors that occur during regex compilation.
//
// Parameters:
// - regex: The regular expression pattern to search for
// - s: The string to search within
//
// Returns:
// - string: The first matched substring, or an empty string if no match
// - error: Any error that occurred during regex compilation
func mustRegexFind(regex string, s string) (string, error) {
r, err := regexp.Compile(regex)
if err != nil {
@ -39,11 +98,34 @@ func mustRegexFind(regex string, s string) (string, error) {
return r.FindString(s), nil
}
// regexReplaceAll replaces all matches of a regular expression with a replacement string.
// It panics if the regex pattern cannot be compiled.
// The replacement string can contain $1, $2, etc. for submatches.
//
// Parameters:
// - regex: The regular expression pattern to search for
// - s: The string to search within
// - repl: The replacement string (can contain $1, $2, etc. for submatches)
//
// Returns:
// - string: The resulting string after all replacements
func regexReplaceAll(regex string, s string, repl string) string {
r := regexp.MustCompile(regex)
return r.ReplaceAllString(s, repl)
}
// mustRegexReplaceAll replaces all matches of a regular expression with a replacement string.
// Unlike regexReplaceAll, this function returns any errors that occur during regex compilation.
// The replacement string can contain $1, $2, etc. for submatches.
//
// Parameters:
// - regex: The regular expression pattern to search for
// - s: The string to search within
// - repl: The replacement string (can contain $1, $2, etc. for submatches)
//
// Returns:
// - string: The resulting string after all replacements
// - error: Any error that occurred during regex compilation
func mustRegexReplaceAll(regex string, s string, repl string) (string, error) {
r, err := regexp.Compile(regex)
if err != nil {
@ -52,11 +134,34 @@ func mustRegexReplaceAll(regex string, s string, repl string) (string, error) {
return r.ReplaceAllString(s, repl), nil
}
// regexReplaceAllLiteral replaces all matches of a regular expression with a literal replacement string.
// It panics if the regex pattern cannot be compiled.
// Unlike regexReplaceAll, the replacement string is used literally (no $1, $2 processing).
//
// Parameters:
// - regex: The regular expression pattern to search for
// - s: The string to search within
// - repl: The literal replacement string
//
// Returns:
// - string: The resulting string after all replacements
func regexReplaceAllLiteral(regex string, s string, repl string) string {
r := regexp.MustCompile(regex)
return r.ReplaceAllLiteralString(s, repl)
}
// mustRegexReplaceAllLiteral replaces all matches of a regular expression with a literal replacement string.
// Unlike regexReplaceAllLiteral, this function returns any errors that occur during regex compilation.
// The replacement string is used literally (no $1, $2 processing).
//
// Parameters:
// - regex: The regular expression pattern to search for
// - s: The string to search within
// - repl: The literal replacement string
//
// Returns:
// - string: The resulting string after all replacements
// - error: Any error that occurred during regex compilation
func mustRegexReplaceAllLiteral(regex string, s string, repl string) (string, error) {
r, err := regexp.Compile(regex)
if err != nil {
@ -65,11 +170,32 @@ func mustRegexReplaceAllLiteral(regex string, s string, repl string) (string, er
return r.ReplaceAllLiteralString(s, repl), nil
}
// regexSplit splits a string by a regular expression pattern.
// It panics if the regex pattern cannot be compiled.
//
// Parameters:
// - regex: The regular expression pattern to split on
// - s: The string to split
// - n: The maximum number of substrings to return (negative means all substrings)
//
// Returns:
// - []string: A slice containing the substrings between regex matches
func regexSplit(regex string, s string, n int) []string {
r := regexp.MustCompile(regex)
return r.Split(s, n)
}
// mustRegexSplit splits a string by a regular expression pattern.
// Unlike regexSplit, this function returns any errors that occur during regex compilation.
//
// Parameters:
// - regex: The regular expression pattern to split on
// - s: The string to split
// - n: The maximum number of substrings to return (negative means all substrings)
//
// Returns:
// - []string: A slice containing the substrings between regex matches
// - error: Any error that occurred during regex compilation
func mustRegexSplit(regex string, s string, n int) ([]string, error) {
r, err := regexp.Compile(regex)
if err != nil {
@ -78,6 +204,14 @@ func mustRegexSplit(regex string, s string, n int) ([]string, error) {
return r.Split(s, n), nil
}
// regexQuoteMeta escapes all regular expression metacharacters in a string.
// This is useful when you want to use a string as a literal in a regular expression.
//
// Parameters:
// - s: The string to escape
//
// Returns:
// - string: The escaped string with all regex metacharacters quoted
func regexQuoteMeta(s string) string {
return regexp.QuoteMeta(s)
}

View file

@ -11,10 +11,25 @@ import (
"strings"
)
// base64encode encodes a string to base64 using standard encoding.
//
// Parameters:
// - v: The string to encode
//
// Returns:
// - string: The base64 encoded string
func base64encode(v string) string {
return base64.StdEncoding.EncodeToString([]byte(v))
}
// base64decode decodes a base64 encoded string.
// If the input is not valid base64, it returns the error message as a string.
//
// Parameters:
// - v: The base64 encoded string to decode
//
// Returns:
// - string: The decoded string, or an error message if decoding fails
func base64decode(v string) string {
data, err := base64.StdEncoding.DecodeString(v)
if err != nil {
@ -23,10 +38,25 @@ func base64decode(v string) string {
return string(data)
}
// base32encode encodes a string to base32 using standard encoding.
//
// Parameters:
// - v: The string to encode
//
// Returns:
// - string: The base32 encoded string
func base32encode(v string) string {
return base32.StdEncoding.EncodeToString([]byte(v))
}
// base32decode decodes a base32 encoded string.
// If the input is not valid base32, it returns the error message as a string.
//
// Parameters:
// - v: The base32 encoded string to decode
//
// Returns:
// - string: The decoded string, or an error message if decoding fails
func base32decode(v string) string {
data, err := base32.StdEncoding.DecodeString(v)
if err != nil {
@ -35,6 +65,14 @@ func base32decode(v string) string {
return string(data)
}
// quote adds double quotes around each non-nil string in the input and joins them with spaces.
// This uses Go's %q formatter which handles escaping special characters.
//
// Parameters:
// - str: A variadic list of values to quote
//
// Returns:
// - string: The quoted strings joined with spaces
func quote(str ...any) string {
out := make([]string, 0, len(str))
for _, s := range str {
@ -45,6 +83,14 @@ func quote(str ...any) string {
return strings.Join(out, " ")
}
// squote adds single quotes around each non-nil value in the input and joins them with spaces.
// Unlike quote, this doesn't escape special characters.
//
// Parameters:
// - str: A variadic list of values to quote
//
// Returns:
// - string: The single-quoted values joined with spaces
func squote(str ...any) string {
out := make([]string, 0, len(str))
for _, s := range str {
@ -55,25 +101,69 @@ func squote(str ...any) string {
return strings.Join(out, " ")
}
// cat concatenates all non-nil values into a single string.
// Nil values are removed before concatenation.
//
// Parameters:
// - v: A variadic list of values to concatenate
//
// Returns:
// - string: The concatenated string
func cat(v ...any) string {
v = removeNilElements(v)
r := strings.TrimSpace(strings.Repeat("%v ", len(v)))
return fmt.Sprintf(r, v...)
}
// indent adds a specified number of spaces at the beginning of each line in a string.
//
// Parameters:
// - spaces: The number of spaces to add
// - v: The string to indent
//
// Returns:
// - string: The indented string
func indent(spaces int, v string) string {
pad := strings.Repeat(" ", spaces)
return pad + strings.Replace(v, "\n", "\n"+pad, -1)
}
// nindent adds a newline followed by an indented string.
// It's a shorthand for "\n" + indent(spaces, v).
//
// Parameters:
// - spaces: The number of spaces to add
// - v: The string to indent
//
// Returns:
// - string: A newline followed by the indented string
func nindent(spaces int, v string) string {
return "\n" + indent(spaces, v)
}
// replace replaces all occurrences of a substring with another substring.
//
// Parameters:
// - old: The substring to replace
// - new: The replacement substring
// - src: The source string
//
// Returns:
// - string: The resulting string after all replacements
func replace(old, new, src string) string {
return strings.Replace(src, old, new, -1)
}
// plural returns the singular or plural form of a word based on the count.
// If count is 1, it returns the singular form, otherwise it returns the plural form.
//
// Parameters:
// - one: The singular form of the word
// - many: The plural form of the word
// - count: The count to determine which form to use
//
// Returns:
// - string: Either the singular or plural form based on the count
func plural(one, many string, count int) string {
if count == 1 {
return one
@ -81,6 +171,19 @@ func plural(one, many string, count int) string {
return many
}
// strslice converts a value to a slice of strings.
// It handles various input types:
// - []string: returned as is
// - []any: converted to []string, skipping nil values
// - arrays and slices: converted to []string, skipping nil values
// - nil: returns an empty slice
// - anything else: returns a single-element slice with the string representation
//
// Parameters:
// - v: The value to convert to a string slice
//
// Returns:
// - []string: A slice of strings
func strslice(v any) []string {
switch v := v.(type) {
case []string:
@ -116,6 +219,14 @@ func strslice(v any) []string {
}
}
// removeNilElements creates a new slice with all nil elements removed.
// This is a helper function used by other functions like cat.
//
// Parameters:
// - v: The slice to process
//
// Returns:
// - []any: A new slice with all nil elements removed
func removeNilElements(v []any) []any {
newSlice := make([]any, 0, len(v))
for _, i := range v {
@ -126,6 +237,19 @@ func removeNilElements(v []any) []any {
return newSlice
}
// strval converts any value to a string.
// It handles various types:
// - string: returned as is
// - []byte: converted to string
// - error: returns the error message
// - fmt.Stringer: calls the String() method
// - anything else: uses fmt.Sprintf("%v", v)
//
// Parameters:
// - v: The value to convert to a string
//
// Returns:
// - string: The string representation of the value
func strval(v any) string {
switch v := v.(type) {
case string:
@ -141,6 +265,17 @@ func strval(v any) string {
}
}
// trunc truncates a string to a specified length.
// If c is positive, it returns the first c characters.
// If c is negative, it returns the last |c| characters.
// If the string is shorter than the requested length, it returns the original string.
//
// Parameters:
// - c: The number of characters to keep (positive from start, negative from end)
// - s: The string to truncate
//
// Returns:
// - string: The truncated string
func trunc(c int, s string) string {
if c < 0 && len(s)+c > 0 {
return s[len(s)+c:]
@ -151,14 +286,40 @@ func trunc(c int, s string) string {
return s
}
// title converts a string to title case.
// This uses the English language rules for capitalization.
//
// Parameters:
// - s: The string to convert
//
// Returns:
// - string: The string in title case
func title(s string) string {
return cases.Title(language.English).String(s)
}
// join concatenates the elements of a slice with a separator.
// The input is first converted to a string slice using strslice.
//
// Parameters:
// - sep: The separator to use between elements
// - v: The value to join (will be converted to a string slice)
//
// Returns:
// - string: The joined string
func join(sep string, v any) string {
return strings.Join(strslice(v), sep)
}
// split splits a string by a separator and returns a map.
// The keys in the map are "_0", "_1", etc., corresponding to the position of each part.
//
// Parameters:
// - sep: The separator to split on
// - orig: The string to split
//
// Returns:
// - map[string]string: A map with keys "_0", "_1", etc. and values being the split parts
func split(sep, orig string) map[string]string {
parts := strings.Split(orig, sep)
res := make(map[string]string, len(parts))
@ -168,10 +329,30 @@ func split(sep, orig string) map[string]string {
return res
}
// splitList splits a string by a separator and returns a slice.
// This is a simple wrapper around strings.Split.
//
// Parameters:
// - sep: The separator to split on
// - orig: The string to split
//
// Returns:
// - []string: A slice containing the split parts
func splitList(sep, orig string) []string {
return strings.Split(orig, sep)
}
// splitn splits a string by a separator with a limit and returns a map.
// The keys in the map are "_0", "_1", etc., corresponding to the position of each part.
// It will split the string into at most n parts.
//
// Parameters:
// - sep: The separator to split on
// - n: The maximum number of parts to return
// - orig: The string to split
//
// Returns:
// - map[string]string: A map with keys "_0", "_1", etc. and values being the split parts
func splitn(sep string, n int, orig string) map[string]string {
parts := strings.SplitN(orig, sep, n)
res := make(map[string]string, len(parts))
@ -182,12 +363,20 @@ func splitn(sep string, n int, orig string) map[string]string {
}
// substring creates a substring of the given string.
// It extracts a portion of a string based on start and end indices.
//
// If start is < 0, this calls string[:end].
// Parameters:
// - start: The starting index (inclusive)
// - end: The ending index (exclusive)
// - s: The source string
//
// If start is >= 0 and end < 0 or end bigger than s length, this calls string[start:]
// Behavior:
// - If start < 0, returns s[:end]
// - If start >= 0 and end < 0 or end > len(s), returns s[start:]
// - Otherwise, returns s[start:end]
//
// Otherwise, this calls string[start, end].
// Returns:
// - string: The extracted substring
func substring(start, end int, s string) string {
if start < 0 {
return s[:end]
@ -198,6 +387,19 @@ func substring(start, end int, s string) string {
return s[start:end]
}
// repeat creates a new string by repeating the input string a specified number of times.
// It has safety limits to prevent excessive memory usage or infinite loops.
//
// Parameters:
// - count: The number of times to repeat the string
// - str: The string to repeat
//
// Returns:
// - string: The repeated string
//
// Panics:
// - If count exceeds loopExecutionLimit
// - If the resulting string length would exceed stringLengthLimit
func repeat(count int, str string) string {
if count > loopExecutionLimit {
panic(fmt.Sprintf("repeat count %d exceeds limit of %d", count, loopExecutionLimit))
@ -207,26 +409,79 @@ func repeat(count int, str string) string {
return strings.Repeat(str, count)
}
// trimAll removes all leading and trailing characters contained in the cutset.
// Note that the parameter order is reversed from the standard strings.Trim function.
//
// Parameters:
// - a: The cutset of characters to remove
// - b: The string to trim
//
// Returns:
// - string: The trimmed string
func trimAll(a, b string) string {
return strings.Trim(b, a)
}
// trimPrefix removes the specified prefix from a string.
// If the string doesn't start with the prefix, it returns the original string.
// Note that the parameter order is reversed from the standard strings.TrimPrefix function.
//
// Parameters:
// - a: The prefix to remove
// - b: The string to trim
//
// Returns:
// - string: The string with the prefix removed, or the original string if it doesn't start with the prefix
func trimPrefix(a, b string) string {
return strings.TrimPrefix(b, a)
}
// trimSuffix removes the specified suffix from a string.
// If the string doesn't end with the suffix, it returns the original string.
// Note that the parameter order is reversed from the standard strings.TrimSuffix function.
//
// Parameters:
// - a: The suffix to remove
// - b: The string to trim
//
// Returns:
// - string: The string with the suffix removed, or the original string if it doesn't end with the suffix
func trimSuffix(a, b string) string {
return strings.TrimSuffix(b, a)
}
// contains checks if a string contains a substring.
//
// Parameters:
// - substr: The substring to search for
// - str: The string to search in
//
// Returns:
// - bool: True if str contains substr, false otherwise
func contains(substr string, str string) bool {
return strings.Contains(str, substr)
}
// hasPrefix checks if a string starts with a specified prefix.
//
// Parameters:
// - substr: The prefix to check for
// - str: The string to check
//
// Returns:
// - bool: True if str starts with substr, false otherwise
func hasPrefix(substr string, str string) bool {
return strings.HasPrefix(str, substr)
}
// hasSuffix checks if a string ends with a specified suffix.
//
// Parameters:
// - substr: The suffix to check for
// - str: The string to check
//
// Returns:
// - bool: True if str ends with substr, false otherwise
func hasSuffix(substr string, str string) bool {
return strings.HasSuffix(str, substr)
}

View file

@ -60,7 +60,6 @@ func urlJoin(d map[string]any) string {
}
user = tempURL.User
}
resURL.User = user
return resURL.String()
}