Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 107 additions & 1 deletion filters/standard_filters.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import (
"math"
"net/url"
"reflect"
"strconv"
"regexp"
"strconv"
"strings"
"time"
"unicode"
Expand Down Expand Up @@ -172,6 +172,8 @@ func AddStandardFilters(fd FilterDictionary) { //nolint: gocyclo
return a[len(a)-1]
})
fd.AddFilter("uniq", uniqFilter)
fd.AddFilter("where", whereFilter)
fd.AddFilter("sum", sumFilter)

// date filters
fd.AddFilter("date", func(t time.Time, format func(string) string) (string, error) {
Expand Down Expand Up @@ -259,11 +261,43 @@ func AddStandardFilters(fd FilterDictionary) { //nolint: gocyclo

return math.Floor(n*exp+0.5) / exp
})
fd.AddFilter("at_least", func(a, b any) any {
if isIntegerType(a) && isIntegerType(b) {
if toInt64(a) < toInt64(b) {
return b
}
return a
}
if toFloat64(a) < toFloat64(b) {
return b
}
return a
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Return numeric result in at_least/at_most

These filters compare operands numerically but then return the original operand (a/b) instead of a coerced number. For inputs like numeric strings, at_least/at_most can therefore return a string (for example, "6") even though they are documented and implemented elsewhere as number filters, which makes downstream type-sensitive behavior inconsistent; the same pattern appears in at_most immediately below and should be fixed there too.

Useful? React with 👍 / 👎.

})
fd.AddFilter("at_most", func(a, b any) any {
if isIntegerType(a) && isIntegerType(b) {
if toInt64(a) > toInt64(b) {
return b
}
return a
}
if toFloat64(a) > toFloat64(b) {
return b
}
return a
})

// sequence filters
fd.AddFilter("size", values.Length)

// string filters
fd.AddFilter("pluralize", func(count any, singular, plural string) string {
if toFloat64(count) == 1.0 {
return singular
}
return plural
})
fd.AddFilter("handleize", handleizeFilter)
fd.AddFilter("handle", handleizeFilter)
fd.AddFilter("append", func(s, suffix string) string {
return s + suffix
})
Expand Down Expand Up @@ -293,10 +327,24 @@ func AddStandardFilters(fd FilterDictionary) { //nolint: gocyclo
fd.AddFilter("remove_first", func(s, old string) string {
return strings.Replace(s, old, "", 1)
})
fd.AddFilter("remove_last", func(s, old string) string {
i := strings.LastIndex(s, old)
if i < 0 {
return s
}
return s[:i] + s[i+len(old):]
})
fd.AddFilter("replace", strings.ReplaceAll)
fd.AddFilter("replace_first", func(s, old, n string) string {
return strings.Replace(s, old, n, 1)
})
fd.AddFilter("replace_last", func(s, old, n string) string {
i := strings.LastIndex(s, old)
if i < 0 {
return s
}
return s[:i] + n + s[i+len(old):]
})
fd.AddFilter("sort_natural", sortNaturalFilter)
fd.AddFilter("slice", func(v interface{}, start int, length func(int) int) interface{} {
// Are we in the []byte case? Transform []byte to string
Expand Down Expand Up @@ -473,6 +521,64 @@ func uniqFilter(a []any) (result []any) {
return
}

var handleizeRe = regexp.MustCompile(`[^a-z0-9]+`)

func handleizeFilter(s string) string {
s = strings.ToLower(s)
s = handleizeRe.ReplaceAllString(s, "-")
s = strings.Trim(s, "-")
return s
}

func whereFilter(a []any, key string, targetValue func(any) any) (result []any) {
keyValue := values.ValueOf(key)
target := targetValue(nil)
Comment thread
michaelhvisser marked this conversation as resolved.
for _, obj := range a {
value := values.ValueOf(obj)
prop := value.PropertyValue(keyValue).Interface()
if target == nil {
// One-arg form: truthy check
if prop != nil && prop != false {
result = append(result, obj)
}
} else {
// Two-arg form: equality check — skip nil props to avoid
// reflect.TypeOf(nil) panic in eqItems
if prop != nil && eqItems(prop, target) {
Comment thread
michaelhvisser marked this conversation as resolved.
Outdated
result = append(result, obj)
}
}
}
return
}

func sumFilter(a []any, key func(string) string) any {
prop := key("")
allInts := true
var intTotal int64
var floatTotal float64
for _, item := range a {
if prop != "" {
v := values.ValueOf(item)
item = v.PropertyValue(values.ValueOf(prop)).Interface()
}
if allInts && isIntegerType(item) {
intTotal += toInt64(item)
} else {
if allInts {
// Switch to float, carry over the int total so far
floatTotal = float64(intTotal)
allInts = false
}
floatTotal += toFloat64(item)
}
}
if allInts {
return intTotal
}
return floatTotal
}

func eqItems(a, b any) bool {
if reflect.TypeOf(a).Comparable() && reflect.TypeOf(b).Comparable() {
return a == b
Expand Down
41 changes: 41 additions & 0 deletions filters/standard_filters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,15 @@ var filterTests = []struct {
{`dup_ints | uniq | join`, "1 2 3"},
{`dup_strings | uniq | join`, "one two three"},
{`dup_maps | uniq | map: "name" | join`, "m1 m2 m3"},
// where
{`products | where: "available" | map: "title" | join: ", "`, "Shirt, Pants"},
{`products | where: "type", "Shirt" | map: "title" | join: ", "`, "Shirt"},
{`products | where: "price", 10.0 | map: "title" | join: ", "`, "Shirt"},
// sum
{`"1,2,3" | split: "," | sum`, 6.0},
{`prices | sum`, int64(30)},
{`products | sum: "price"`, 30.0},

{`mixed_case_array | sort_natural | join`, "a B c"},
{`mixed_case_hash_values | sort_natural: 'key' | map: 'key' | join`, "a B c"},

Expand Down Expand Up @@ -89,6 +98,22 @@ var filterTests = []struct {
{`"Straße" | size`, 6},

// string filters
// pluralize
{`1 | pluralize: "item", "items"`, "item"},
{`2 | pluralize: "item", "items"`, "items"},
{`0 | pluralize: "item", "items"`, "items"},
{`1.0 | pluralize: "item", "items"`, "item"},
{`1.5 | pluralize: "item", "items"`, "items"},
{`"1" | pluralize: "item", "items"`, "item"},
{`"2" | pluralize: "item", "items"`, "items"},

// handleize / handle
{`"100% M & Ms!!!" | handleize`, "100-m-ms"},
{`"Hello World" | handleize`, "hello-world"},
{`"" | handleize`, ""},
{`"---already---" | handleize`, "already"},
{`"Hello World" | handle`, "hello-world"},

{`"Take my protein pills and put my helmet on" | replace: "my", "your"`, "Take your protein pills and put your helmet on"},
{`"Take my protein pills and put my helmet on" | replace_first: "my", "your"`, "Take your protein pills and put my helmet on"},
{`"/my/fancy/url" | append: ".html"`, "/my/fancy/url.html"},
Expand All @@ -104,6 +129,10 @@ var filterTests = []struct {
{`"apples, oranges, and bananas" | prepend: "Some fruit: "`, "Some fruit: apples, oranges, and bananas"},
{`"I strained to see the train through the rain" | remove: "rain"`, "I sted to see the t through the "},
{`"I strained to see the train through the rain" | remove_first: "rain"`, "I sted to see the train through the rain"},
{`"Hello Hello Hello" | remove_last: "Hello"`, "Hello Hello "},
{`"Hello" | remove_last: "xyz"`, "Hello"},
{`"Hello Hello Hello" | replace_last: "Hello", "Goodbye"`, "Hello Hello Goodbye"},
{`"Hello" | replace_last: "xyz", "abc"`, "Hello"},

{`"Liquid" | slice: 0`, "L"},
{`"Liquid
Expand Down Expand Up @@ -228,6 +257,12 @@ Liquid" | slice: 2, 4`, "quid"},
{`str_int | plus: 1`, 11.0},
{`str_float | plus: 1.0`, 4.5},

// at_least / at_most
{`4 | at_least: 5`, 5},
{`6 | at_least: 5`, 6},
{`4 | at_most: 5`, 4},
{`6 | at_most: 5`, 5},

{`3 | modulo: 2`, 1.0},
{`24 | modulo: 7`, 3.0},
// {`183.357 | modulo: 12 | `, 3.357}, // TODO test suit use inexact
Expand Down Expand Up @@ -282,6 +317,12 @@ var filterTestBindings = map[string]any{
{"weight": 3},
{"weight": nil},
},
"products": []map[string]any{
{"title": "Shirt", "type": "Shirt", "price": 10.0, "available": true},
{"title": "Pants", "type": "Pants", "price": 20.0, "available": true},
{"title": "Hat", "type": "Hat", "price": nil, "available": false},
},
"prices": []any{10, 20},
"string_with_newlines": "\nHello\nthere\n",
"dup_ints": []int{1, 2, 1, 3},
"dup_strings": []string{"one", "two", "one", "three"},
Expand Down
2 changes: 1 addition & 1 deletion parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func TestSourceError(t *testing.T) {
loc := SourceLoc{Pathname: "test.html", LineNo: 5}
token := Token{
SourceLoc: loc,
Source: "{% bad %}",
Source: "{% bad %}",
}

err := Errorf(&token, "something went wrong")
Expand Down
1 change: 0 additions & 1 deletion tags/iteration_tags.go
Original file line number Diff line number Diff line change
Expand Up @@ -352,4 +352,3 @@ type reverseWrapper struct {

func (w reverseWrapper) Len() int { return w.i.Len() }
func (w reverseWrapper) Index(i int) any { return w.i.Index(w.i.Len() - 1 - i) }