Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
112 changes: 111 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,47 @@ 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) {
ai, bi := toInt64(a), toInt64(b)
if ai < bi {
return bi
}
return ai
}
af, bf := toFloat64(a), toFloat64(b)
if af < bf {
return bf
}
return af
})
fd.AddFilter("at_most", func(a, b any) any {
if isIntegerType(a) && isIntegerType(b) {
ai, bi := toInt64(a), toInt64(b)
if ai > bi {
return bi
}
return ai
}
af, bf := toFloat64(a), toFloat64(b)
if af > bf {
return bf
}
return af
})

// 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 +331,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 +525,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)
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 using Liquid-compatible
// comparison (handles nil, mixed int/float, etc.)
if values.Equal(prop, target) {
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
42 changes: 42 additions & 0 deletions filters/standard_filters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ 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"},
{`products | where: "price", 10 | 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 +99,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 +130,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 +258,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`, int64(5)},
{`6 | at_least: 5`, int64(6)},
{`4 | at_most: 5`, int64(4)},
{`6 | at_most: 5`, int64(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 +318,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) }