Skip to content

Commit 30211ac

Browse files
committed
Implement some filters
1 parent e3425cc commit 30211ac

File tree

5 files changed

+239
-31
lines changed

5 files changed

+239
-31
lines changed

chunks/render_test.go

Lines changed: 85 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
"github.com/stretchr/testify/require"
99
)
1010

11-
var chunkTests = []struct{ in, expected string }{
11+
var renderTests = []struct{ in, expected string }{
1212
{"{{12}}", "12"},
1313
{"{{x}}", "123"},
1414
{"{{page.title}}", "Introduction"},
@@ -38,32 +38,111 @@ var chunkTests = []struct{ in, expected string }{
3838

3939
{"{%assign av = 1%}{{av}}", "1"},
4040
{"{%assign av = obj.a%}{{av}}", "1"},
41-
// "{% assign var = obj.a | sort: 'weight' %}"
41+
4242
// {"{%for a in ar%}{{a}} {{%endfor%}", "first second third "},
4343
}
4444

45-
var chunkTestContext = Context{map[string]interface{}{
45+
var filterTests = []struct{ in, expected string }{
46+
// filters
47+
// {{ product_price | default: 2.99 }}
48+
49+
// list filters
50+
// {{ site.pages | map: 'category' | compact | join "," %}
51+
// {% assign my_array = "apples, oranges, peaches, plums" | split: ", " %}{{ my_array.first }}
52+
// {`{{"John, Paul, George, Ringo" | split: ", " | join: "and"}}`, "John and Paul and George and Ringo"},
53+
{`{{ animals | sort | join }}`, "Sally Snake, giraffe, octopus, zebra"},
54+
// {`{{ animals | sort | join: "," }}`, "Sally Snake, giraffe, octopus, zebra"},
55+
// join, last, map, slice, sort, sort_natural, reverse, size, uniq
56+
57+
// string filters
58+
// {{ "/my/fancy/url" | append: ".html" }}
59+
// {% assign filename = "/index.html" %}{{ "website.com" | append: filename }}
60+
61+
// {{ "title" | capitalize }}
62+
// {{ "my great title" | capitalize }}
63+
64+
// {{ "Parker Moore" | downcase }}
65+
66+
// {{ "Have you read 'James & the Giant Peach'?" | escape }}
67+
// {{ "1 < 2 & 3" | escape_once }}
68+
// {{ "1 &lt; 2 &amp; 3" | escape_once }}
69+
70+
// lstrip, newline_to_br, prepend, remove, remove_first, replace, replace_first
71+
// rstrip, split, strip, strip_html, strip_newlines, truncate, truncatewords, upcase
72+
// url_decode, url_encode
73+
74+
// number filters
75+
// {{ -17 | abs }}
76+
// {{ 4 | abs }}
77+
// {{ "-19.86" | abs }}
78+
79+
// {{ 1.2 | ceil }}
80+
// {{ 2.0 | ceil }}
81+
// {{ 183.357 | ceil }}
82+
// {{ "3.5" | ceil }}
83+
84+
// {{ 16 | divided_by: 4 }}
85+
// {{ 5 | divided_by: 3 }}
86+
// {{ 20 | divided_by: 7.0 }}
87+
88+
// {{ 1.2 | floor }}
89+
// {{ 2.0 | floor }}
90+
// {{ 183.357 | floor }}
91+
// minus, modulo, plus, round,times
92+
93+
// date filters
94+
// {{ article.published_at | date: "%a, %b %d, %y" }}
95+
// {{ article.published_at | date: "%Y" }}
96+
// {{ "March 14, 2016" | date: "%b %d, %y" }}
97+
// {{ "now" | date: "%Y-%m-%d %H:%M" }
98+
}
99+
100+
var renderTestContext = Context{map[string]interface{}{
46101
"x": 123,
47102
"obj": map[string]interface{}{
48103
"a": 1,
49104
},
105+
"animals": []string{"zebra", "octopus", "giraffe", "Sally Snake"},
106+
"pages": []map[string]interface{}{
107+
{"category": "business"},
108+
{"category": "celebrities"},
109+
{},
110+
{"category": "lifestyle"},
111+
{"category": "sports"},
112+
{},
113+
{"category": "technology"},
114+
},
50115
"ar": []string{"first", "second", "third"},
51116
"page": map[string]interface{}{
52117
"title": "Introduction",
53118
},
54119
},
55120
}
56121

57-
func TestChunkParser(t *testing.T) {
58-
for i, test := range chunkTests {
122+
func TestRender(t *testing.T) {
123+
for i, test := range renderTests {
59124
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
60125
tokens := Scan(test.in, "")
61126
// fmt.Println(tokens)
62127
ast, err := Parse(tokens)
63128
require.NoErrorf(t, err, test.in)
64129
// fmt.Println(MustYAML(ast))
65130
buf := new(bytes.Buffer)
66-
err = ast.Render(buf, chunkTestContext)
131+
err = ast.Render(buf, renderTestContext)
132+
require.NoErrorf(t, err, test.in)
133+
require.Equalf(t, test.expected, buf.String(), test.in)
134+
})
135+
}
136+
}
137+
138+
func TestFilters(t *testing.T) {
139+
for i, test := range filterTests {
140+
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
141+
tokens := Scan(test.in, "")
142+
ast, err := Parse(tokens)
143+
require.NoErrorf(t, err, test.in)
144+
buf := new(bytes.Buffer)
145+
err = ast.Render(buf, renderTestContext)
67146
require.NoErrorf(t, err, test.in)
68147
require.Equalf(t, test.expected, buf.String(), test.in)
69148
})

expressions/expressions.y

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ expr:
5151
return nil
5252
}
5353
}
54+
| expr '|' IDENTIFIER { $$ = makeFilter($1, $3) }
5455
| expr '[' expr ']' {
5556
e, i := $1, $3
5657
$$ = func(ctx Context) interface{} {

expressions/filters.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package expressions
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
"sort"
7+
"strings"
8+
)
9+
10+
type valueFn func(Context) interface{}
11+
12+
func joinFilter(in []interface{}) interface{} {
13+
a := make([]string, len(in))
14+
for i, x := range in {
15+
a[i] = fmt.Sprint(x)
16+
}
17+
return strings.Join(a, ", ")
18+
}
19+
20+
func sortFilter(in []interface{}) []interface{} {
21+
a := make([]interface{}, len(in))
22+
for i, x := range in {
23+
a[i] = x
24+
}
25+
sort.Sort(sortable(a))
26+
return a
27+
}
28+
29+
var filters = map[string]interface{}{
30+
"join": joinFilter,
31+
"sort": sortFilter,
32+
}
33+
34+
func makeFilter(f valueFn, name string) valueFn {
35+
fn, ok := filters[name]
36+
if !ok {
37+
panic(fmt.Errorf("unknown filter: %s", name))
38+
}
39+
fr := reflect.ValueOf(fn)
40+
return func(ctx Context) interface{} {
41+
args := []interface{}{f(ctx)}
42+
in := convertArguments(fr, args)
43+
r := fr.Call(in)[0]
44+
return r.Interface()
45+
}
46+
}

expressions/generics.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,77 @@ import (
55
"reflect"
66
)
77

8+
type sortable []interface{}
9+
10+
// Len is part of sort.Interface.
11+
func (s sortable) Len() int {
12+
return len(s)
13+
}
14+
15+
// Swap is part of sort.Interface.
16+
func (s sortable) Swap(i, j int) {
17+
s[i], s[j] = s[j], s[i]
18+
}
19+
20+
// Less is part of sort.Interface.
21+
func (s sortable) Less(i, j int) bool {
22+
return genericSameTypeCompare(s[i], s[j]) < 0
23+
}
24+
25+
// Convert val to the type. This is a more aggressive conversion, that will
26+
// recursively create new map and slice values as necessary. It doesn't
27+
// handle circular references.
28+
func convertType(val interface{}, t reflect.Type) reflect.Value {
29+
r := reflect.ValueOf(val)
30+
if r.Type().ConvertibleTo(t) {
31+
return r.Convert(t)
32+
}
33+
switch t.Kind() {
34+
case reflect.Slice:
35+
if r.Kind() != reflect.Array && r.Kind() != reflect.Slice {
36+
break
37+
}
38+
x := reflect.MakeSlice(t, 0, r.Len())
39+
for i := 0; i < r.Len(); i++ {
40+
c := convertType(r.Index(i).Interface(), t.Elem())
41+
x = reflect.Append(x, c)
42+
}
43+
return x
44+
}
45+
panic(fmt.Errorf("convertType: can't convert %v to %v", val, t))
46+
}
47+
48+
// Convert args to match the input types of fr, which should be a function reflection.
49+
func convertArguments(fv reflect.Value, args []interface{}) []reflect.Value {
50+
rt := fv.Type()
51+
rs := make([]reflect.Value, rt.NumIn())
52+
for i, arg := range args {
53+
if i < rt.NumIn() {
54+
rs[i] = convertType(arg, rt.In(i))
55+
}
56+
}
57+
return rs
58+
}
59+
60+
func genericSameTypeCompare(av, bv interface{}) int {
61+
a, b := reflect.ValueOf(av), reflect.ValueOf(bv)
62+
if a.Kind() != b.Kind() {
63+
panic(fmt.Errorf("different types: %v and %v", a, b))
64+
}
65+
if a == b {
66+
return 0
67+
}
68+
switch a.Kind() {
69+
case reflect.String:
70+
if a.String() < b.String() {
71+
return -1
72+
}
73+
default:
74+
panic(fmt.Errorf("unimplemented generic comparison for %s", a.Kind()))
75+
}
76+
return 1
77+
}
78+
879
func GenericCompare(a, b reflect.Value) int {
980
if a.Interface() == b.Interface() {
1081
return 0

expressions/y.go

Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ var yyToknames = [...]string{
4141
"'>'",
4242
"';'",
4343
"'='",
44+
"'|'",
4445
"'['",
4546
"']'",
4647
}
@@ -59,45 +60,46 @@ var yyExca = [...]int{
5960

6061
const yyPrivate = 57344
6162

62-
const yyLast = 30
63+
const yyLast = 35
6364

6465
var yyAct = [...]int{
6566

66-
4, 11, 9, 12, 13, 9, 7, 10, 22, 1,
67-
10, 16, 17, 18, 19, 20, 9, 14, 9, 5,
68-
6, 10, 21, 10, 5, 6, 15, 3, 8, 2,
67+
12, 9, 13, 14, 4, 9, 10, 11, 9, 7,
68+
10, 11, 23, 10, 11, 15, 18, 19, 20, 21,
69+
22, 9, 5, 6, 24, 3, 10, 11, 5, 6,
70+
17, 16, 8, 1, 2,
6971
}
7072
var yyPact = [...]int{
7173

72-
20, -1000, -6, 23, -7, -1000, -1000, -1000, 4, 21,
73-
15, 15, 15, 15, 15, -1000, 7, 9, 9, 9,
74-
-4, -1000, -1000,
74+
18, -1000, -3, 27, -8, -1000, -1000, -1000, 2, 26,
75+
25, 24, 24, 24, 24, 24, -1000, -1000, -4, -1,
76+
-1, -1, 12, -1000, -1000,
7577
}
7678
var yyPgo = [...]int{
7779

78-
0, 0, 29, 9,
80+
0, 4, 34, 33,
7981
}
8082
var yyR1 = [...]int{
8183

82-
0, 3, 3, 1, 1, 1, 1, 2, 2, 2,
83-
2,
84+
0, 3, 3, 1, 1, 1, 1, 1, 2, 2,
85+
2, 2,
8486
}
8587
var yyR2 = [...]int{
8688

87-
0, 2, 5, 1, 1, 3, 4, 1, 3, 3,
88-
3,
89+
0, 2, 5, 1, 1, 3, 3, 4, 1, 3,
90+
3, 3,
8991
}
9092
var yyChk = [...]int{
9193

9294
-1000, -3, -2, 7, -1, 4, 5, 12, 5, 9,
93-
14, 8, 10, 11, 13, 5, -1, -1, -1, -1,
94-
-1, 15, 12,
95+
14, 15, 8, 10, 11, 13, 5, 5, -1, -1,
96+
-1, -1, -1, 16, 12,
9597
}
9698
var yyDef = [...]int{
9799

98-
0, -2, 0, 0, 7, 3, 4, 1, 0, 0,
99-
0, 0, 0, 0, 0, 5, 0, 8, 9, 10,
100-
0, 6, 2,
100+
0, -2, 0, 0, 8, 3, 4, 1, 0, 0,
101+
0, 0, 0, 0, 0, 0, 5, 6, 0, 9,
102+
10, 11, 0, 7, 2,
101103
}
102104
var yyTok1 = [...]int{
103105

@@ -110,7 +112,10 @@ var yyTok1 = [...]int{
110112
10, 13, 11, 3, 3, 3, 3, 3, 3, 3,
111113
3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
112114
3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
113-
3, 14, 3, 15,
115+
3, 15, 3, 16, 3, 3, 3, 3, 3, 3,
116+
3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
117+
3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
118+
3, 3, 3, 3, 14,
114119
}
115120
var yyTok2 = [...]int{
116121

@@ -506,8 +511,14 @@ yydefault:
506511
}
507512
}
508513
case 6:
509-
yyDollar = yyS[yypt-4 : yypt+1]
514+
yyDollar = yyS[yypt-3 : yypt+1]
510515
//line expressions.y:54
516+
{
517+
yyVAL.f = makeFilter(yyDollar[1].f, yyDollar[3].name)
518+
}
519+
case 7:
520+
yyDollar = yyS[yypt-4 : yypt+1]
521+
//line expressions.y:55
511522
{
512523
e, i := yyDollar[1].f, yyDollar[3].f
513524
yyVAL.f = func(ctx Context) interface{} {
@@ -526,29 +537,29 @@ yydefault:
526537
return nil
527538
}
528539
}
529-
case 8:
540+
case 9:
530541
yyDollar = yyS[yypt-3 : yypt+1]
531-
//line expressions.y:76
542+
//line expressions.y:77
532543
{
533544
a, b := yyDollar[1].f, yyDollar[3].f
534545
yyVAL.f = func(ctx Context) interface{} {
535546
aref, bref := reflect.ValueOf(a(ctx)), reflect.ValueOf(b(ctx))
536547
return GenericCompare(aref, bref) == 0
537548
}
538549
}
539-
case 9:
550+
case 10:
540551
yyDollar = yyS[yypt-3 : yypt+1]
541-
//line expressions.y:83
552+
//line expressions.y:84
542553
{
543554
a, b := yyDollar[1].f, yyDollar[3].f
544555
yyVAL.f = func(ctx Context) interface{} {
545556
aref, bref := reflect.ValueOf(a(ctx)), reflect.ValueOf(b(ctx))
546557
return GenericCompare(aref, bref) < 0
547558
}
548559
}
549-
case 10:
560+
case 11:
550561
yyDollar = yyS[yypt-3 : yypt+1]
551-
//line expressions.y:90
562+
//line expressions.y:91
552563
{
553564
a, b := yyDollar[1].f, yyDollar[3].f
554565
yyVAL.f = func(ctx Context) interface{} {

0 commit comments

Comments
 (0)