diff --git a/expressions/expressions.y b/expressions/expressions.y index cf99e5f..501ace9 100644 --- a/expressions/expressions.y +++ b/expressions/expressions.y @@ -2,7 +2,6 @@ package expressions import ( "fmt" - "math" "github.com/osteele/liquid/values" ) @@ -26,7 +25,7 @@ func init() { loopmods loopModifiers filter_params []valueFn } -%type expr rel filtered cond int_or_var loop_expr +%type expr rel filtered cond %type filter_params %type exprs expr2 %type cycle @@ -84,25 +83,13 @@ string: LITERAL { $$ = s }; -loop: IDENTIFIER IN loop_expr loop_modifiers { +loop: IDENTIFIER IN filtered loop_modifiers { name, expr, mods := $1, $3, $4 $$ = Loop{name, &expression{expr}, mods} } ; -loop_expr : '(' int_or_var DOTDOT int_or_var ')' { - $$ = makeRangeExpr($2, $4) -} -| filtered -; - -// TODO DRY w/ expr -int_or_var: - LITERAL { val := $1; $$ = func(Context) values.Value { return values.ValueOf(val) } } -| IDENTIFIER { name := $1; $$ = func(ctx Context) values.Value { return values.ValueOf(ctx.Get(name)) } } -; - -loop_modifiers: /* empty */ { $$ = loopModifiers{Cols: math.MaxInt32} } +loop_modifiers: /* empty */ { $$ = loopModifiers{} } | loop_modifiers IDENTIFIER { switch $2 { case "reversed": @@ -112,26 +99,14 @@ loop_modifiers: /* empty */ { $$ = loopModifiers{Cols: math.MaxInt32} } } $$ = $1 } -| loop_modifiers KEYWORD LITERAL { // TODO can this be a variable? - switch $2 { +| loop_modifiers KEYWORD expr { + switch $2 { case "cols": - cols, ok := $3.(int) - if !ok { - panic(SyntaxError(fmt.Sprintf("loop cols must an integer"))) - } - $1.Cols = cols + $1.Cols = &expression{$3} case "limit": - limit, ok := $3.(int) - if !ok { - panic(SyntaxError(fmt.Sprintf("loop limit must an integer"))) - } - $1.Limit = &limit + $1.Limit = &expression{$3} case "offset": - offset, ok := $3.(int) - if !ok { - panic(SyntaxError(fmt.Sprintf("loop offset must an integer"))) - } - $1.Offset = offset + $1.Offset = &expression{$3} default: panic(SyntaxError(fmt.Sprintf("undefined loop modifier %q", $2))) } @@ -144,6 +119,7 @@ expr: | IDENTIFIER { name := $1; $$ = func(ctx Context) values.Value { return values.ValueOf(ctx.Get(name)) } } | expr PROPERTY { $$ = makeObjectPropertyExpr($1, $2) } | expr '[' expr ']' { $$ = makeIndexExpr($1, $3) } +| '(' expr DOTDOT expr ')' { $$ = makeRangeExpr($2, $4) } | '(' cond ')' { $$ = $2 } ; diff --git a/expressions/expressions_test.go b/expressions/expressions_test.go index f381c52..ef4fd3c 100644 --- a/expressions/expressions_test.go +++ b/expressions/expressions_test.go @@ -6,6 +6,7 @@ import ( "strings" "testing" + "github.com/osteele/liquid/values" "github.com/stretchr/testify/require" ) @@ -27,6 +28,7 @@ var evaluatorTests = []struct { // Attributes {`hash.a`, "first"}, {`hash.b.c`, "d"}, + {`hash["b"].c`, "d"}, {`hash.x`, nil}, {`fruits.first`, "apples"}, {`fruits.last`, "plums"}, @@ -44,7 +46,14 @@ var evaluatorTests = []struct { {`hash[1]`, nil}, {`hash.c[0]`, "r"}, + // Range + {`(1..5)`, values.NewRange(1, 5)}, + {`(1..range.end)`, values.NewRange(1, 5)}, + {`(1..range["end"])`, values.NewRange(1, 5)}, + {`(range.begin..range.end)`, values.NewRange(1, 5)}, + // Expressions + {`(1)`, 1}, {`(n)`, 123}, // Operators @@ -116,6 +125,10 @@ var evaluatorTestBindings = (map[string]interface{}{ "c": []string{"r", "g", "b"}, }, "hash_with_size_key": map[string]interface{}{"size": "key_value"}, + "range": map[string]interface{}{ + "begin": 1, + "end": 5, + }, }) func TestEvaluateString(t *testing.T) { diff --git a/expressions/statements.go b/expressions/statements.go index f2d7634..6c41e12 100644 --- a/expressions/statements.go +++ b/expressions/statements.go @@ -34,10 +34,10 @@ type Loop struct { } type loopModifiers struct { - Limit *int - Offset int + Limit Expression + Offset Expression + Cols Expression Reversed bool - Cols int } // A When is a parse of a {% when %} clause diff --git a/expressions/statements_test.go b/expressions/statements_test.go index 9a14a03..cdd74c8 100644 --- a/expressions/statements_test.go +++ b/expressions/statements_test.go @@ -10,6 +10,7 @@ func TestParseStatement(t *testing.T) { stmt, err := ParseStatement(AssignStatementSelector, "a = b") require.NoError(t, err) require.Equal(t, "a", stmt.Assignment.Variable) + require.Implements(t, (*Expression)(nil), stmt.Assignment.ValueFn) stmt, err = ParseStatement(CycleStatementSelector, "'a', 'b'") require.NoError(t, err) @@ -27,9 +28,12 @@ func TestParseStatement(t *testing.T) { require.NoError(t, err) require.Equal(t, "x", stmt.Loop.Variable) require.True(t, stmt.Loop.Reversed) - require.Equal(t, 2, stmt.Loop.Offset) + + require.Nil(t, stmt.Loop.Cols) require.NotNil(t, stmt.Loop.Limit) - require.Equal(t, 3, *stmt.Loop.Limit) + require.Implements(t, (*Expression)(nil), stmt.Loop.Limit) + require.NotNil(t, stmt.Loop.Offset) + require.Implements(t, (*Expression)(nil), stmt.Loop.Offset) stmt, err = ParseStatement(WhenStatementSelector, "a, b") require.NoError(t, err) diff --git a/expressions/y.go b/expressions/y.go index abf2201..5d2c2ee 100644 --- a/expressions/y.go +++ b/expressions/y.go @@ -9,7 +9,6 @@ import __yyfmt__ "fmt" import ( "fmt" "github.com/osteele/liquid/values" - "math" ) func init() { @@ -18,7 +17,7 @@ func init() { _ = "" } -//line expressions.y:16 +//line expressions.y:15 type yySymType struct { yys int name string @@ -81,10 +80,10 @@ var yyToknames = [...]string{ "'='", "':'", "','", - "'('", - "')'", "'['", "']'", + "'('", + "')'", } var yyStatenames = [...]string{} @@ -94,115 +93,109 @@ const yyErrCode = 2 const yyInitialStackSize = 16 //line yacctab:1 -var yyExca = [...]int{ +var yyExca = [...]int8{ -1, 1, 1, -1, -2, 0, - -1, 75, - 20, 18, - -2, 23, - -1, 76, - 20, 19, - -2, 24, } const yyPrivate = 57344 -const yyLast = 104 - -var yyAct = [...]int{ - 9, 74, 46, 41, 8, 87, 78, 23, 14, 15, - 18, 10, 11, 25, 42, 3, 4, 5, 6, 25, - 37, 58, 10, 11, 40, 42, 45, 50, 51, 52, - 53, 54, 55, 56, 57, 43, 12, 26, 60, 38, - 81, 24, 59, 26, 69, 60, 25, 12, 66, 65, - 68, 61, 24, 62, 44, 70, 25, 75, 76, 10, - 11, 27, 28, 31, 32, 71, 72, 47, 33, 77, - 26, 7, 30, 29, 79, 80, 21, 14, 15, 82, - 26, 16, 12, 84, 64, 13, 35, 36, 48, 49, - 85, 86, 83, 19, 34, 2, 1, 73, 20, 39, - 17, 22, 67, 63, +const yyLast = 106 + +var yyAct = [...]int8{ + 9, 47, 42, 18, 8, 14, 15, 23, 10, 11, + 10, 11, 76, 34, 3, 4, 5, 6, 25, 25, + 60, 41, 43, 43, 38, 46, 44, 51, 52, 53, + 54, 55, 56, 57, 58, 12, 25, 12, 39, 62, + 26, 26, 24, 61, 77, 63, 62, 64, 45, 66, + 65, 68, 24, 14, 15, 71, 7, 25, 26, 69, + 70, 13, 27, 28, 31, 32, 72, 73, 75, 33, + 59, 36, 37, 30, 29, 25, 25, 80, 2, 26, + 81, 27, 28, 31, 32, 78, 79, 21, 33, 49, + 50, 35, 30, 29, 16, 19, 48, 26, 26, 1, + 74, 20, 40, 17, 22, 67, } -var yyPact = [...]int{ - 7, -1000, 60, 76, 89, 71, 18, -1000, 19, 49, - -1000, -1000, 18, -1000, 18, 18, -6, 14, -3, -1000, - 10, 38, 1, 39, 83, -1000, 18, 18, 18, 18, - 18, 18, 18, 18, -9, -1000, -1000, 18, -1000, -1000, - 89, -1000, 89, -1000, 55, -1000, -1000, 18, -1000, 18, - 12, 6, 6, 6, 6, 6, 6, 6, -1000, 30, - 6, -14, -14, -1000, 53, 19, 39, -22, 6, -1000, - -1000, -1000, -1000, 69, 20, -1000, -1000, -1000, 18, -1000, - 88, 86, 6, -1000, -25, -1000, -1000, -1000, +var yyPact = [...]int16{ + 6, -1000, 36, 89, 91, 82, 4, -1000, 20, 69, + -1000, -1000, 4, -1000, 4, 4, -2, 13, -6, -1000, + 1, 32, 0, 68, 84, -1000, 4, 4, 4, 4, + 4, 4, 4, 4, 50, -12, -1000, -1000, 4, -1000, + -1000, 91, -1000, 91, -1000, 4, -1000, -1000, 4, -1000, + 4, 29, 11, 11, 11, 11, 11, 11, 11, 4, + -1000, 30, 11, -5, -5, 20, 68, -16, 11, -1000, + 12, -1000, -1000, -1000, 80, -1000, 4, -1000, -1000, 4, + 11, 11, } -var yyPgo = [...]int{ - 0, 0, 71, 4, 94, 1, 103, 102, 101, 2, - 100, 99, 3, 98, 97, 10, 96, +var yyPgo = [...]int8{ + 0, 0, 56, 4, 78, 105, 104, 1, 103, 102, + 2, 101, 100, 3, 99, } -var yyR1 = [...]int{ - 0, 16, 16, 16, 16, 16, 10, 11, 11, 12, - 12, 8, 9, 9, 15, 13, 6, 6, 5, 5, - 14, 14, 14, 1, 1, 1, 1, 1, 3, 3, - 3, 7, 7, 2, 2, 2, 2, 2, 2, 2, - 2, 4, 4, 4, +var yyR1 = [...]int8{ + 0, 14, 14, 14, 14, 14, 8, 9, 9, 10, + 10, 6, 7, 7, 13, 11, 12, 12, 12, 1, + 1, 1, 1, 1, 1, 3, 3, 3, 5, 5, + 2, 2, 2, 2, 2, 2, 2, 2, 4, 4, + 4, } -var yyR2 = [...]int{ +var yyR2 = [...]int8{ 0, 2, 5, 3, 3, 3, 2, 3, 1, 0, - 3, 2, 0, 3, 1, 4, 5, 1, 1, 1, - 0, 2, 3, 1, 1, 2, 4, 3, 1, 3, - 4, 1, 3, 1, 3, 3, 3, 3, 3, 3, - 3, 1, 3, 3, + 3, 2, 0, 3, 1, 4, 0, 2, 3, 1, + 1, 2, 4, 5, 3, 1, 3, 4, 1, 3, + 1, 3, 3, 3, 3, 3, 3, 3, 1, 3, + 3, } -var yyChk = [...]int{ - -1000, -16, -4, 8, 9, 10, 11, -2, -3, -1, - 4, 5, 29, 25, 17, 18, 5, -10, -15, 4, - -13, 5, -8, -1, 22, 7, 31, 12, 13, 24, - 23, 14, 15, 19, -4, -2, -2, 26, 25, -11, - 27, -12, 28, 25, 16, 25, -9, 28, 5, 6, - -1, -1, -1, -1, -1, -1, -1, -1, 30, -3, - -1, -15, -15, -6, 29, -3, -1, -7, -1, 32, - 25, -12, -12, -14, -5, 4, 5, -9, 28, 5, - 6, 20, -1, 4, -5, 4, 5, 30, +var yyChk = [...]int16{ + -1000, -14, -4, 8, 9, 10, 11, -2, -3, -1, + 4, 5, 31, 25, 17, 18, 5, -8, -13, 4, + -11, 5, -6, -1, 22, 7, 29, 12, 13, 24, + 23, 14, 15, 19, -1, -4, -2, -2, 26, 25, + -9, 27, -10, 28, 25, 16, 25, -7, 28, 5, + 6, -1, -1, -1, -1, -1, -1, -1, -1, 20, + 32, -3, -1, -13, -13, -3, -1, -5, -1, 30, + -1, 25, -10, -10, -12, -7, 28, 32, 5, 6, + -1, -1, } -var yyDef = [...]int{ - 0, -2, 0, 0, 0, 0, 0, 41, 33, 28, - 23, 24, 0, 1, 0, 0, 0, 0, 9, 14, - 0, 0, 0, 12, 0, 25, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 42, 43, 0, 3, 6, - 0, 8, 0, 4, 0, 5, 11, 0, 29, 0, - 0, 34, 35, 36, 37, 38, 39, 40, 27, 0, - 28, 9, 9, 20, 0, 17, 12, 30, 31, 26, - 2, 7, 10, 15, 0, -2, -2, 13, 0, 21, - 0, 0, 32, 22, 0, 18, 19, 16, +var yyDef = [...]int8{ + 0, -2, 0, 0, 0, 0, 0, 38, 30, 25, + 19, 20, 0, 1, 0, 0, 0, 0, 9, 14, + 0, 0, 0, 12, 0, 21, 0, 0, 0, 0, + 0, 0, 0, 0, 25, 0, 39, 40, 0, 3, + 6, 0, 8, 0, 4, 0, 5, 11, 0, 26, + 0, 0, 31, 32, 33, 34, 35, 36, 37, 0, + 24, 0, 25, 9, 9, 16, 12, 27, 28, 22, + 0, 2, 7, 10, 15, 13, 0, 23, 17, 0, + 29, 18, } -var yyTok1 = [...]int{ +var yyTok1 = [...]int8{ 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, - 29, 30, 3, 3, 28, 3, 21, 3, 3, 3, + 31, 32, 3, 3, 28, 3, 21, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 27, 25, 23, 26, 24, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, - 3, 31, 3, 32, 3, 3, 3, 3, 3, 3, + 3, 29, 3, 30, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 22, } -var yyTok2 = [...]int{ +var yyTok2 = [...]int8{ 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, } -var yyTok3 = [...]int{ +var yyTok3 = [...]int8{ 0, } @@ -284,9 +277,9 @@ func yyErrorMessage(state, lookAhead int) string { expected := make([]int, 0, 4) // Look for shiftable tokens. - base := yyPact[state] + base := int(yyPact[state]) for tok := TOKSTART; tok-1 < len(yyToknames); tok++ { - if n := base + tok; n >= 0 && n < yyLast && yyChk[yyAct[n]] == tok { + if n := base + tok; n >= 0 && n < yyLast && int(yyChk[int(yyAct[n])]) == tok { if len(expected) == cap(expected) { return res } @@ -296,13 +289,13 @@ func yyErrorMessage(state, lookAhead int) string { if yyDef[state] == -2 { i := 0 - for yyExca[i] != -1 || yyExca[i+1] != state { + for yyExca[i] != -1 || int(yyExca[i+1]) != state { i += 2 } // Look for tokens that we accept or reduce. for i += 2; yyExca[i] >= 0; i += 2 { - tok := yyExca[i] + tok := int(yyExca[i]) if tok < TOKSTART || yyExca[i+1] == 0 { continue } @@ -333,30 +326,30 @@ func yylex1(lex yyLexer, lval *yySymType) (char, token int) { token = 0 char = lex.Lex(lval) if char <= 0 { - token = yyTok1[0] + token = int(yyTok1[0]) goto out } if char < len(yyTok1) { - token = yyTok1[char] + token = int(yyTok1[char]) goto out } if char >= yyPrivate { if char < yyPrivate+len(yyTok2) { - token = yyTok2[char-yyPrivate] + token = int(yyTok2[char-yyPrivate]) goto out } } for i := 0; i < len(yyTok3); i += 2 { - token = yyTok3[i+0] + token = int(yyTok3[i+0]) if token == char { - token = yyTok3[i+1] + token = int(yyTok3[i+1]) goto out } } out: if token == 0 { - token = yyTok2[1] /* unknown char */ + token = int(yyTok2[1]) /* unknown char */ } if yyDebug >= 3 { __yyfmt__.Printf("lex %s(%d)\n", yyTokname(token), uint(char)) @@ -411,7 +404,7 @@ yystack: yyS[yyp].yys = yystate yynewstate: - yyn = yyPact[yystate] + yyn = int(yyPact[yystate]) if yyn <= yyFlag { goto yydefault /* simple state */ } @@ -422,8 +415,8 @@ yynewstate: if yyn < 0 || yyn >= yyLast { goto yydefault } - yyn = yyAct[yyn] - if yyChk[yyn] == yytoken { /* valid shift */ + yyn = int(yyAct[yyn]) + if int(yyChk[yyn]) == yytoken { /* valid shift */ yyrcvr.char = -1 yytoken = -1 yyVAL = yyrcvr.lval @@ -436,7 +429,7 @@ yynewstate: yydefault: /* default state action */ - yyn = yyDef[yystate] + yyn = int(yyDef[yystate]) if yyn == -2 { if yyrcvr.char < 0 { yyrcvr.char, yytoken = yylex1(yylex, &yyrcvr.lval) @@ -445,18 +438,18 @@ yydefault: /* look through exception table */ xi := 0 for { - if yyExca[xi+0] == -1 && yyExca[xi+1] == yystate { + if yyExca[xi+0] == -1 && int(yyExca[xi+1]) == yystate { break } xi += 2 } for xi += 2; ; xi += 2 { - yyn = yyExca[xi+0] + yyn = int(yyExca[xi+0]) if yyn < 0 || yyn == yytoken { break } } - yyn = yyExca[xi+1] + yyn = int(yyExca[xi+1]) if yyn < 0 { goto ret0 } @@ -478,10 +471,10 @@ yydefault: /* find a state where "error" is a legal shift action */ for yyp >= 0 { - yyn = yyPact[yyS[yyp].yys] + yyErrCode + yyn = int(yyPact[yyS[yyp].yys]) + yyErrCode if yyn >= 0 && yyn < yyLast { - yystate = yyAct[yyn] /* simulate a shift of "error" */ - if yyChk[yystate] == yyErrCode { + yystate = int(yyAct[yyn]) /* simulate a shift of "error" */ + if int(yyChk[yystate]) == yyErrCode { goto yystack } } @@ -517,7 +510,7 @@ yydefault: yypt := yyp _ = yypt // guard against "declared and not used" - yyp -= yyR2[yyn] + yyp -= int(yyR2[yyn]) // yyp is now the index of $0. Perform the default action. Iff the // reduced production is ε, $1 is possibly out of range. if yyp+1 >= len(yyS) { @@ -528,16 +521,16 @@ yydefault: yyVAL = yyS[yyp+1] /* consult goto table to find next state */ - yyn = yyR1[yyn] - yyg := yyPgo[yyn] + yyn = int(yyR1[yyn]) + yyg := int(yyPgo[yyn]) yyj := yyg + yyS[yyp].yys + 1 if yyj >= yyLast { - yystate = yyAct[yyg] + yystate = int(yyAct[yyg]) } else { - yystate = yyAct[yyj] - if yyChk[yystate] != -yyn { - yystate = yyAct[yyg] + yystate = int(yyAct[yyj]) + if int(yyChk[yystate]) != -yyn { + yystate = int(yyAct[yyg]) } } // dummy call; replaced with literal code @@ -545,87 +538,87 @@ yydefault: case 1: yyDollar = yyS[yypt-2 : yypt+1] -//line expressions.y:46 +//line expressions.y:45 { yylex.(*lexer).val = yyDollar[1].f } case 2: yyDollar = yyS[yypt-5 : yypt+1] -//line expressions.y:47 +//line expressions.y:46 { yylex.(*lexer).Assignment = Assignment{yyDollar[2].name, &expression{yyDollar[4].f}} } case 3: yyDollar = yyS[yypt-3 : yypt+1] -//line expressions.y:50 +//line expressions.y:49 { yylex.(*lexer).Cycle = yyDollar[2].cycle } case 4: yyDollar = yyS[yypt-3 : yypt+1] -//line expressions.y:51 +//line expressions.y:50 { yylex.(*lexer).Loop = yyDollar[2].loop } case 5: yyDollar = yyS[yypt-3 : yypt+1] -//line expressions.y:52 +//line expressions.y:51 { yylex.(*lexer).When = When{yyDollar[2].exprs} } case 6: yyDollar = yyS[yypt-2 : yypt+1] -//line expressions.y:55 +//line expressions.y:54 { yyVAL.cycle = yyDollar[2].cyclefn(yyDollar[1].s) } case 7: yyDollar = yyS[yypt-3 : yypt+1] -//line expressions.y:58 +//line expressions.y:57 { h, t := yyDollar[2].s, yyDollar[3].ss yyVAL.cyclefn = func(g string) Cycle { return Cycle{g, append([]string{h}, t...)} } } case 8: yyDollar = yyS[yypt-1 : yypt+1] -//line expressions.y:62 +//line expressions.y:61 { vals := yyDollar[1].ss yyVAL.cyclefn = func(h string) Cycle { return Cycle{Values: append([]string{h}, vals...)} } } case 9: yyDollar = yyS[yypt-0 : yypt+1] -//line expressions.y:69 +//line expressions.y:68 { yyVAL.ss = []string{} } case 10: yyDollar = yyS[yypt-3 : yypt+1] -//line expressions.y:70 +//line expressions.y:69 { yyVAL.ss = append([]string{yyDollar[2].s}, yyDollar[3].ss...) } case 11: yyDollar = yyS[yypt-2 : yypt+1] -//line expressions.y:73 +//line expressions.y:72 { yyVAL.exprs = append([]Expression{&expression{yyDollar[1].f}}, yyDollar[2].exprs...) } case 12: yyDollar = yyS[yypt-0 : yypt+1] -//line expressions.y:75 +//line expressions.y:74 { yyVAL.exprs = []Expression{} } case 13: yyDollar = yyS[yypt-3 : yypt+1] -//line expressions.y:76 +//line expressions.y:75 { yyVAL.exprs = append([]Expression{&expression{yyDollar[2].f}}, yyDollar[3].exprs...) } case 14: yyDollar = yyS[yypt-1 : yypt+1] -//line expressions.y:79 +//line expressions.y:78 { s, ok := yyDollar[1].val.(string) if !ok { @@ -635,40 +628,20 @@ yydefault: } case 15: yyDollar = yyS[yypt-4 : yypt+1] -//line expressions.y:87 +//line expressions.y:86 { name, expr, mods := yyDollar[1].name, yyDollar[3].f, yyDollar[4].loopmods yyVAL.loop = Loop{name, &expression{expr}, mods} } case 16: - yyDollar = yyS[yypt-5 : yypt+1] -//line expressions.y:93 - { - yyVAL.f = makeRangeExpr(yyDollar[2].f, yyDollar[4].f) - } - case 18: - yyDollar = yyS[yypt-1 : yypt+1] -//line expressions.y:101 - { - val := yyDollar[1].val - yyVAL.f = func(Context) values.Value { return values.ValueOf(val) } - } - case 19: - yyDollar = yyS[yypt-1 : yypt+1] -//line expressions.y:102 - { - name := yyDollar[1].name - yyVAL.f = func(ctx Context) values.Value { return values.ValueOf(ctx.Get(name)) } - } - case 20: yyDollar = yyS[yypt-0 : yypt+1] -//line expressions.y:105 +//line expressions.y:92 { - yyVAL.loopmods = loopModifiers{Cols: math.MaxInt32} + yyVAL.loopmods = loopModifiers{} } - case 21: + case 17: yyDollar = yyS[yypt-2 : yypt+1] -//line expressions.y:106 +//line expressions.y:93 { switch yyDollar[2].name { case "reversed": @@ -678,93 +651,87 @@ yydefault: } yyVAL.loopmods = yyDollar[1].loopmods } - case 22: + case 18: yyDollar = yyS[yypt-3 : yypt+1] -//line expressions.y:115 - { // TODO can this be a variable? +//line expressions.y:102 + { switch yyDollar[2].name { case "cols": - cols, ok := yyDollar[3].val.(int) - if !ok { - panic(SyntaxError(fmt.Sprintf("loop cols must an integer"))) - } - yyDollar[1].loopmods.Cols = cols + yyDollar[1].loopmods.Cols = &expression{yyDollar[3].f} case "limit": - limit, ok := yyDollar[3].val.(int) - if !ok { - panic(SyntaxError(fmt.Sprintf("loop limit must an integer"))) - } - yyDollar[1].loopmods.Limit = &limit + yyDollar[1].loopmods.Limit = &expression{yyDollar[3].f} case "offset": - offset, ok := yyDollar[3].val.(int) - if !ok { - panic(SyntaxError(fmt.Sprintf("loop offset must an integer"))) - } - yyDollar[1].loopmods.Offset = offset + yyDollar[1].loopmods.Offset = &expression{yyDollar[3].f} default: panic(SyntaxError(fmt.Sprintf("undefined loop modifier %q", yyDollar[2].name))) } yyVAL.loopmods = yyDollar[1].loopmods } - case 23: + case 19: yyDollar = yyS[yypt-1 : yypt+1] -//line expressions.y:143 +//line expressions.y:118 { val := yyDollar[1].val yyVAL.f = func(Context) values.Value { return values.ValueOf(val) } } - case 24: + case 20: yyDollar = yyS[yypt-1 : yypt+1] -//line expressions.y:144 +//line expressions.y:119 { name := yyDollar[1].name yyVAL.f = func(ctx Context) values.Value { return values.ValueOf(ctx.Get(name)) } } - case 25: + case 21: yyDollar = yyS[yypt-2 : yypt+1] -//line expressions.y:145 +//line expressions.y:120 { yyVAL.f = makeObjectPropertyExpr(yyDollar[1].f, yyDollar[2].name) } - case 26: + case 22: yyDollar = yyS[yypt-4 : yypt+1] -//line expressions.y:146 +//line expressions.y:121 { yyVAL.f = makeIndexExpr(yyDollar[1].f, yyDollar[3].f) } - case 27: + case 23: + yyDollar = yyS[yypt-5 : yypt+1] +//line expressions.y:122 + { + yyVAL.f = makeRangeExpr(yyDollar[2].f, yyDollar[4].f) + } + case 24: yyDollar = yyS[yypt-3 : yypt+1] -//line expressions.y:147 +//line expressions.y:123 { yyVAL.f = yyDollar[2].f } - case 29: + case 26: yyDollar = yyS[yypt-3 : yypt+1] -//line expressions.y:152 +//line expressions.y:128 { yyVAL.f = makeFilter(yyDollar[1].f, yyDollar[3].name, nil) } - case 30: + case 27: yyDollar = yyS[yypt-4 : yypt+1] -//line expressions.y:153 +//line expressions.y:129 { yyVAL.f = makeFilter(yyDollar[1].f, yyDollar[3].name, yyDollar[4].filter_params) } - case 31: + case 28: yyDollar = yyS[yypt-1 : yypt+1] -//line expressions.y:157 +//line expressions.y:133 { yyVAL.filter_params = []valueFn{yyDollar[1].f} } - case 32: + case 29: yyDollar = yyS[yypt-3 : yypt+1] -//line expressions.y:159 +//line expressions.y:135 { yyVAL.filter_params = append(yyDollar[1].filter_params, yyDollar[3].f) } - case 34: + case 31: yyDollar = yyS[yypt-3 : yypt+1] -//line expressions.y:163 +//line expressions.y:139 { fa, fb := yyDollar[1].f, yyDollar[3].f yyVAL.f = func(ctx Context) values.Value { @@ -772,9 +739,9 @@ yydefault: return values.ValueOf(a.Equal(b)) } } - case 35: + case 32: yyDollar = yyS[yypt-3 : yypt+1] -//line expressions.y:170 +//line expressions.y:146 { fa, fb := yyDollar[1].f, yyDollar[3].f yyVAL.f = func(ctx Context) values.Value { @@ -782,9 +749,9 @@ yydefault: return values.ValueOf(!a.Equal(b)) } } - case 36: + case 33: yyDollar = yyS[yypt-3 : yypt+1] -//line expressions.y:177 +//line expressions.y:153 { fa, fb := yyDollar[1].f, yyDollar[3].f yyVAL.f = func(ctx Context) values.Value { @@ -792,9 +759,9 @@ yydefault: return values.ValueOf(b.Less(a)) } } - case 37: + case 34: yyDollar = yyS[yypt-3 : yypt+1] -//line expressions.y:184 +//line expressions.y:160 { fa, fb := yyDollar[1].f, yyDollar[3].f yyVAL.f = func(ctx Context) values.Value { @@ -802,9 +769,9 @@ yydefault: return values.ValueOf(a.Less(b)) } } - case 38: + case 35: yyDollar = yyS[yypt-3 : yypt+1] -//line expressions.y:191 +//line expressions.y:167 { fa, fb := yyDollar[1].f, yyDollar[3].f yyVAL.f = func(ctx Context) values.Value { @@ -812,9 +779,9 @@ yydefault: return values.ValueOf(b.Less(a) || a.Equal(b)) } } - case 39: + case 36: yyDollar = yyS[yypt-3 : yypt+1] -//line expressions.y:198 +//line expressions.y:174 { fa, fb := yyDollar[1].f, yyDollar[3].f yyVAL.f = func(ctx Context) values.Value { @@ -822,24 +789,24 @@ yydefault: return values.ValueOf(a.Less(b) || a.Equal(b)) } } - case 40: + case 37: yyDollar = yyS[yypt-3 : yypt+1] -//line expressions.y:205 +//line expressions.y:181 { yyVAL.f = makeContainsExpr(yyDollar[1].f, yyDollar[3].f) } - case 42: + case 39: yyDollar = yyS[yypt-3 : yypt+1] -//line expressions.y:210 +//line expressions.y:186 { fa, fb := yyDollar[1].f, yyDollar[3].f yyVAL.f = func(ctx Context) values.Value { return values.ValueOf(fa(ctx).Test() && fb(ctx).Test()) } } - case 43: + case 40: yyDollar = yyS[yypt-3 : yypt+1] -//line expressions.y:216 +//line expressions.y:192 { fa, fb := yyDollar[1].f, yyDollar[3].f yyVAL.f = func(ctx Context) values.Value { diff --git a/tags/iteration_tags.go b/tags/iteration_tags.go index 581ff85..702e443 100644 --- a/tags/iteration_tags.go +++ b/tags/iteration_tags.go @@ -3,6 +3,7 @@ package tags import ( "fmt" "io" + "math" "reflect" "sort" @@ -66,17 +67,16 @@ func loopTagCompiler(node render.BlockNode) (func(io.Writer, render.Context) err if err != nil { return nil, err } - loop := stmt.Loop - dec := makeLoopDecorator(node.Name, loop) - return loopRenderer{loop, dec}.render, nil + return loopRenderer{stmt.Loop, node.Name}.render, nil } type loopRenderer struct { expressions.Loop - loopDecorator + tagName string } func (loop loopRenderer) render(w io.Writer, ctx render.Context) error { + // loop modifiers val, err := ctx.Evaluate(loop.Expr) if err != nil { return err @@ -85,7 +85,17 @@ func (loop loopRenderer) render(w io.Writer, ctx render.Context) error { if iter == nil { return nil } - iter = applyLoopModifiers(loop.Loop, iter) + iter, err = applyLoopModifiers(loop.Loop, ctx, iter) + if err != nil { + return err + } + + // loop decorator + decorator, err := makeLoopDecorator(loop, ctx) + if err != nil { + return err + } + // shallow-bind the loop variables; restore on exit defer func(index, forloop interface{}) { ctx.Set(forloopVarName, index) @@ -105,9 +115,9 @@ loop: "length": len, ".cycles": cycleMap, }) - loop.before(w, i) + decorator.before(w, i) err := ctx.RenderChildren(w) - loop.after(w, i, len) + decorator.after(w, i, len) switch { case err == nil: // fall through @@ -122,11 +132,24 @@ loop: return nil } -func makeLoopDecorator(tagName string, loop expressions.Loop) loopDecorator { - if tagName == "tablerow" { - return tableRowDecorator(loop.Cols) +func makeLoopDecorator(loop loopRenderer, ctx render.Context) (loopDecorator, error) { + if loop.tagName == "tablerow" { + if loop.Cols != nil { + val, err := ctx.Evaluate(loop.Cols) + if err != nil { + return nil, err + } + cols, ok := val.(int) + if !ok { + return nil, ctx.Errorf("loop cols must be an integer") + } + if cols > 0 { + return tableRowDecorator(cols), nil + } + } + return tableRowDecorator(math.MaxInt32), nil } - return forLoopDecorator{} + return forLoopDecorator{}, nil } type loopDecorator interface { @@ -166,17 +189,40 @@ func (c tableRowDecorator) after(w io.Writer, i, len int) { } } -func applyLoopModifiers(loop expressions.Loop, iter iterable) iterable { +func applyLoopModifiers(loop expressions.Loop, ctx render.Context, iter iterable) (iterable, error) { if loop.Reversed { iter = reverseWrapper{iter} } - if loop.Offset > 0 { - iter = offsetWrapper{iter, loop.Offset} + + if loop.Offset != nil { + val, err := ctx.Evaluate(loop.Offset) + if err != nil { + return nil, err + } + offset, ok := val.(int) + if !ok { + return nil, ctx.Errorf("loop offset must be an integer") + } + if offset > 0 { + iter = offsetWrapper{iter, offset} + } } + if loop.Limit != nil { - iter = limitWrapper{iter, *loop.Limit} + val, err := ctx.Evaluate(loop.Limit) + if err != nil { + return nil, err + } + limit, ok := val.(int) + if !ok { + return nil, ctx.Errorf("loop limit must be an integer") + } + if limit >= 0 { + iter = limitWrapper{iter, limit} + } } - return iter + + return iter, nil } func makeIterator(value interface{}) iterable { diff --git a/tags/iteration_tags_test.go b/tags/iteration_tags_test.go index d3acfec..78aa555 100644 --- a/tags/iteration_tags_test.go +++ b/tags/iteration_tags_test.go @@ -28,7 +28,13 @@ var iterationTests = []struct{ in, expected string }{ // loop modifiers {`{% for a in array reversed %}{{ a }}.{% endfor %}`, "third.second.first."}, {`{% for a in array limit: 2 %}{{ a }}.{% endfor %}`, "first.second."}, + {`{% for a in array limit: limit %}{{ a }}.{% endfor %}`, "first.second."}, + {`{% for a in array limit: loopmods.limit %}{{ a }}.{% endfor %}`, "first.second."}, + {`{% for a in array limit: loopmods["limit"] %}{{ a }}.{% endfor %}`, "first.second."}, {`{% for a in array offset: 1 %}{{ a }}.{% endfor %}`, "second.third."}, + {`{% for a in array offset: offset %}{{ a }}.{% endfor %}`, "second.third."}, + {`{% for a in array offset: loopmods.offset %}{{ a }}.{% endfor %}`, "second.third."}, + {`{% for a in array offset: loopmods["offset"] %}{{ a }}.{% endfor %}`, "second.third."}, {`{% for a in array reversed limit: 1 %}{{ a }}.{% endfor %}`, "third."}, {`{% for a in array limit: 0 %}{{ a }}.{% endfor %}`, ""}, {`{% for a in array offset: 3 %}{{ a }}.{% endfor %}`, ""}, @@ -93,6 +99,18 @@ var iterationTests = []struct{ in, expected string }{ `Cool ShirtAlien Poster Batman PosterBullseye Shirt Another Classic VinylAwesome Jeans`}, + {`{% tablerow product in products cols: cols %}{{ product }}{% endtablerow %}`, + `Cool ShirtAlien Poster + Batman PosterBullseye Shirt + Another Classic VinylAwesome Jeans`}, + {`{% tablerow product in products cols: loopmods.cols %}{{ product }}{% endtablerow %}`, + `Cool ShirtAlien Poster + Batman PosterBullseye Shirt + Another Classic VinylAwesome Jeans`}, + {`{% tablerow product in products cols: loopmods.cols %}{{ product }}{% endtablerow %}`, + `Cool ShirtAlien Poster + Batman PosterBullseye Shirt + Another Classic VinylAwesome Jeans`}, } var iterationSyntaxErrorTests = []struct{ in, expected string }{ @@ -118,6 +136,10 @@ var iterationTestBindings = map[string]interface{}{ "products": []string{ "Cool Shirt", "Alien Poster", "Batman Poster", "Bullseye Shirt", "Another Classic Vinyl", "Awesome Jeans", }, + "offset": 1, + "limit": 2, + "cols": 2, + "loopmods": map[string]interface{}{"limit": 2, "offset": 1, "cols": 2}, } func TestIterationTags(t *testing.T) {