From cd234476f77efba53ae289b28d763e76a1150a50 Mon Sep 17 00:00:00 2001 From: Oliver Steele Date: Sat, 15 Jul 2017 10:38:12 -0400 Subject: [PATCH] Implement tablerow --- README.md | 1 - expressions/expressions.y | 11 ++++- expressions/statements.go | 1 + expressions/y.go | 91 ++++++++++++++++++++----------------- tags/iteration_tags.go | 47 +++++++++++++++++++ tags/iteration_tags_test.go | 29 +++++++++++- tags/standard_tags.go | 2 +- 7 files changed, 134 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index ce81b8d..d717f05 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,6 @@ The [feature parity board](https://github.com/osteele/liquid/projects/1) lists d In brief, these aren't implemented: -- The `tablerow` tag - Error modes - Whitespace control diff --git a/expressions/expressions.y b/expressions/expressions.y index db2dfeb..5400efd 100644 --- a/expressions/expressions.y +++ b/expressions/expressions.y @@ -1,7 +1,8 @@ %{ package expressions import ( - "fmt" + "fmt" + "math" "github.com/osteele/liquid/evaluator" ) @@ -101,7 +102,7 @@ int_or_var: | IDENTIFIER { name := $1; $$ = func(ctx Context) interface{} { return ctx.Get(name) } } ; -loop_modifiers: /* empty */ { $$ = loopModifiers{} } +loop_modifiers: /* empty */ { $$ = loopModifiers{Cols: math.MaxUint32} } | loop_modifiers IDENTIFIER { switch $2 { case "reversed": @@ -113,6 +114,12 @@ loop_modifiers: /* empty */ { $$ = loopModifiers{} } } | loop_modifiers KEYWORD LITERAL { // TODO can this be a variable? switch $2 { + case "cols": + cols, ok := $3.(int) + if !ok { + panic(ParseError(fmt.Sprintf("loop cols must an integer"))) + } + $1.Cols = cols case "limit": limit, ok := $3.(int) if !ok { diff --git a/expressions/statements.go b/expressions/statements.go index c19fbab..f2d7634 100644 --- a/expressions/statements.go +++ b/expressions/statements.go @@ -37,6 +37,7 @@ type loopModifiers struct { Limit *int Offset int Reversed bool + Cols int } // A When is a parse of a {% when %} clause diff --git a/expressions/y.go b/expressions/y.go index 8bb9f30..14ea2b4 100644 --- a/expressions/y.go +++ b/expressions/y.go @@ -7,15 +7,16 @@ import __yyfmt__ "fmt" import ( "fmt" "github.com/osteele/liquid/evaluator" + "math" ) func init() { // This allows adding and removing references to fmt in the rules below, - // without having to edit the import statement to avoid erorrs each time. + // without having to comment and un-comment the import statement above. _ = fmt.Sprint("") } -//line expressions.y:15 +//line expressions.y:16 type yySymType struct { yys int name string @@ -539,87 +540,87 @@ yydefault: case 1: yyDollar = yyS[yypt-2 : yypt+1] - //line expressions.y:45 + //line expressions.y:46 { yylex.(*lexer).val = yyDollar[1].f } case 2: yyDollar = yyS[yypt-5 : yypt+1] - //line expressions.y:46 + //line expressions.y:47 { yylex.(*lexer).Assignment = Assignment{yyDollar[2].name, &expression{yyDollar[4].f}} } case 3: yyDollar = yyS[yypt-3 : yypt+1] - //line expressions.y:49 + //line expressions.y:50 { yylex.(*lexer).Cycle = yyDollar[2].cycle } case 4: yyDollar = yyS[yypt-3 : yypt+1] - //line expressions.y:50 + //line expressions.y:51 { yylex.(*lexer).Loop = yyDollar[2].loop } case 5: yyDollar = yyS[yypt-3 : yypt+1] - //line expressions.y:51 + //line expressions.y:52 { yylex.(*lexer).When = When{yyDollar[2].exprs} } case 6: yyDollar = yyS[yypt-2 : yypt+1] - //line expressions.y:54 + //line expressions.y:55 { yyVAL.cycle = yyDollar[2].cyclefn(yyDollar[1].s) } case 7: yyDollar = yyS[yypt-3 : yypt+1] - //line expressions.y:57 + //line expressions.y:58 { 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:61 + //line expressions.y:62 { 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:68 + //line expressions.y:69 { yyVAL.ss = []string{} } case 10: yyDollar = yyS[yypt-3 : yypt+1] - //line expressions.y:69 + //line expressions.y:70 { yyVAL.ss = append([]string{yyDollar[2].s}, yyDollar[3].ss...) } case 11: yyDollar = yyS[yypt-2 : yypt+1] - //line expressions.y:72 + //line expressions.y:73 { yyVAL.exprs = append([]Expression{&expression{yyDollar[1].f}}, yyDollar[2].exprs...) } case 12: yyDollar = yyS[yypt-0 : yypt+1] - //line expressions.y:74 + //line expressions.y:75 { yyVAL.exprs = []Expression{} } case 13: yyDollar = yyS[yypt-3 : yypt+1] - //line expressions.y:75 + //line expressions.y:76 { yyVAL.exprs = append([]Expression{&expression{yyDollar[2].f}}, yyDollar[3].exprs...) } case 14: yyDollar = yyS[yypt-1 : yypt+1] - //line expressions.y:78 + //line expressions.y:79 { s, ok := yyDollar[1].val.(string) if !ok { @@ -629,40 +630,40 @@ yydefault: } case 15: yyDollar = yyS[yypt-4 : yypt+1] - //line expressions.y:86 + //line expressions.y:87 { name, expr, mods := yyDollar[1].name, yyDollar[3].f, yyDollar[4].loopmods yyVAL.loop = Loop{name, &expression{expr}, mods} } case 16: yyDollar = yyS[yypt-6 : yypt+1] - //line expressions.y:92 + //line expressions.y:93 { yyVAL.f = makeRangeExpr(yyDollar[2].f, yyDollar[5].f) } case 18: yyDollar = yyS[yypt-1 : yypt+1] - //line expressions.y:100 + //line expressions.y:101 { val := yyDollar[1].val yyVAL.f = func(Context) interface{} { return val } } case 19: yyDollar = yyS[yypt-1 : yypt+1] - //line expressions.y:101 + //line expressions.y:102 { name := yyDollar[1].name yyVAL.f = func(ctx Context) interface{} { return ctx.Get(name) } } case 20: yyDollar = yyS[yypt-0 : yypt+1] - //line expressions.y:104 + //line expressions.y:105 { - yyVAL.loopmods = loopModifiers{} + yyVAL.loopmods = loopModifiers{Cols: math.MaxUint32} } case 21: yyDollar = yyS[yypt-2 : yypt+1] - //line expressions.y:105 + //line expressions.y:106 { switch yyDollar[2].name { case "reversed": @@ -674,9 +675,15 @@ yydefault: } case 22: yyDollar = yyS[yypt-3 : yypt+1] - //line expressions.y:114 + //line expressions.y:115 { // TODO can this be a variable? switch yyDollar[2].name { + case "cols": + cols, ok := yyDollar[3].val.(int) + if !ok { + panic(ParseError(fmt.Sprintf("loop cols must an integer"))) + } + yyDollar[1].loopmods.Cols = cols case "limit": limit, ok := yyDollar[3].val.(int) if !ok { @@ -696,63 +703,63 @@ yydefault: } case 23: yyDollar = yyS[yypt-1 : yypt+1] - //line expressions.y:136 + //line expressions.y:143 { val := yyDollar[1].val yyVAL.f = func(Context) interface{} { return val } } case 24: yyDollar = yyS[yypt-1 : yypt+1] - //line expressions.y:137 + //line expressions.y:144 { name := yyDollar[1].name yyVAL.f = func(ctx Context) interface{} { return ctx.Get(name) } } case 25: yyDollar = yyS[yypt-2 : yypt+1] - //line expressions.y:138 + //line expressions.y:145 { yyVAL.f = makeObjectPropertyExpr(yyDollar[1].f, yyDollar[2].name) } case 26: yyDollar = yyS[yypt-4 : yypt+1] - //line expressions.y:139 + //line expressions.y:146 { yyVAL.f = makeIndexExpr(yyDollar[1].f, yyDollar[3].f) } case 27: yyDollar = yyS[yypt-3 : yypt+1] - //line expressions.y:140 + //line expressions.y:147 { yyVAL.f = yyDollar[2].f } case 29: yyDollar = yyS[yypt-3 : yypt+1] - //line expressions.y:145 + //line expressions.y:152 { yyVAL.f = makeFilter(yyDollar[1].f, yyDollar[3].name, nil) } case 30: yyDollar = yyS[yypt-4 : yypt+1] - //line expressions.y:146 + //line expressions.y:153 { yyVAL.f = makeFilter(yyDollar[1].f, yyDollar[3].name, yyDollar[4].filter_params) } case 31: yyDollar = yyS[yypt-1 : yypt+1] - //line expressions.y:150 + //line expressions.y:157 { yyVAL.filter_params = []valueFn{yyDollar[1].f} } case 32: yyDollar = yyS[yypt-3 : yypt+1] - //line expressions.y:152 + //line expressions.y:159 { yyVAL.filter_params = append(yyDollar[1].filter_params, yyDollar[3].f) } case 34: yyDollar = yyS[yypt-3 : yypt+1] - //line expressions.y:156 + //line expressions.y:163 { fa, fb := yyDollar[1].f, yyDollar[3].f yyVAL.f = func(ctx Context) interface{} { @@ -762,7 +769,7 @@ yydefault: } case 35: yyDollar = yyS[yypt-3 : yypt+1] - //line expressions.y:163 + //line expressions.y:170 { fa, fb := yyDollar[1].f, yyDollar[3].f yyVAL.f = func(ctx Context) interface{} { @@ -772,7 +779,7 @@ yydefault: } case 36: yyDollar = yyS[yypt-3 : yypt+1] - //line expressions.y:170 + //line expressions.y:177 { fa, fb := yyDollar[1].f, yyDollar[3].f yyVAL.f = func(ctx Context) interface{} { @@ -782,7 +789,7 @@ yydefault: } case 37: yyDollar = yyS[yypt-3 : yypt+1] - //line expressions.y:177 + //line expressions.y:184 { fa, fb := yyDollar[1].f, yyDollar[3].f yyVAL.f = func(ctx Context) interface{} { @@ -792,7 +799,7 @@ yydefault: } case 38: yyDollar = yyS[yypt-3 : yypt+1] - //line expressions.y:184 + //line expressions.y:191 { fa, fb := yyDollar[1].f, yyDollar[3].f yyVAL.f = func(ctx Context) interface{} { @@ -802,7 +809,7 @@ yydefault: } case 39: yyDollar = yyS[yypt-3 : yypt+1] - //line expressions.y:191 + //line expressions.y:198 { fa, fb := yyDollar[1].f, yyDollar[3].f yyVAL.f = func(ctx Context) interface{} { @@ -812,13 +819,13 @@ yydefault: } case 40: yyDollar = yyS[yypt-3 : yypt+1] - //line expressions.y:198 + //line expressions.y:205 { yyVAL.f = makeContainsExpr(yyDollar[1].f, yyDollar[3].f) } case 42: yyDollar = yyS[yypt-3 : yypt+1] - //line expressions.y:203 + //line expressions.y:210 { fa, fb := yyDollar[1].f, yyDollar[3].f yyVAL.f = func(ctx Context) interface{} { @@ -827,7 +834,7 @@ yydefault: } case 43: yyDollar = yyS[yypt-3 : yypt+1] - //line expressions.y:209 + //line expressions.y:216 { fa, fb := yyDollar[1].f, yyDollar[3].f yyVAL.f = func(ctx Context) interface{} { diff --git a/tags/iteration_tags.go b/tags/iteration_tags.go index 920ff51..ff55c73 100644 --- a/tags/iteration_tags.go +++ b/tags/iteration_tags.go @@ -69,6 +69,7 @@ func loopTagParser(node render.BlockNode) (func(io.Writer, render.Context) error return nil, err } loop := stmt.Loop + dec := makeLoopDecorator(node.Name, loop) return func(w io.Writer, ctx render.Context) error { val, err := ctx.Evaluate(loop.Expr) if err != nil { @@ -98,7 +99,9 @@ func loopTagParser(node render.BlockNode) (func(io.Writer, render.Context) error "length": len, ".cycles": cycleMap, }) + dec.before(w, i) err := ctx.RenderChildren(w) + dec.after(w, i, len) switch { case err == nil: // fall through @@ -114,6 +117,50 @@ func loopTagParser(node render.BlockNode) (func(io.Writer, render.Context) error }, nil } +func makeLoopDecorator(tagName string, loop expressions.Loop) loopDecorator { + if tagName == "tablerow" { + return tableRowDecorator(loop.Cols) + } + return nullLoopDecorator{} +} + +type loopDecorator interface { + before(io.Writer, int) + after(io.Writer, int, int) +} + +type nullLoopDecorator struct{} + +func (d nullLoopDecorator) before(io.Writer, int) {} +func (d nullLoopDecorator) after(io.Writer, int, int) {} + +type tableRowDecorator int + +func (c tableRowDecorator) before(w io.Writer, i int) { + cols := int(c) + row, col := i/cols, i%cols + if col == 0 { + if _, err := fmt.Fprintf(w, ``, row+1); err != nil { + panic(err) + } + } + if _, err := fmt.Fprintf(w, ``, col+1); err != nil { + panic(err) + } +} + +func (c tableRowDecorator) after(w io.Writer, i, len int) { + cols := int(c) + if _, err := io.WriteString(w, ``); err != nil { + panic(err) + } + if (i+1)%cols == 0 || i+1 == len { + if _, err := io.WriteString(w, ``); err != nil { + panic(err) + } + } +} + func applyLoopModifiers(loop expressions.Loop, iter iterable) iterable { if loop.Reversed { iter = reverseWrapper{iter} diff --git a/tags/iteration_tags_test.go b/tags/iteration_tags_test.go index 7edc9c4..e35b597 100644 --- a/tags/iteration_tags_test.go +++ b/tags/iteration_tags_test.go @@ -4,6 +4,8 @@ import ( "bytes" "fmt" "io/ioutil" + "regexp" + "strings" "testing" "github.com/osteele/liquid/parser" @@ -68,6 +70,20 @@ var iterationTests = []struct{ in, expected string }{ // range {`{% for i in (3 .. 5) %}{{i}}.{% endfor %}`, "3.4.5."}, {`{% for i in (3..5) %}{{i}}.{% endfor %}`, "3.4.5."}, + + // tablerow + {`{% tablerow product in products %}{{ product }}{% endtablerow %}`, + `Cool Shirt + Alien Poster + Batman Poster + Bullseye Shirt + Another Classic Vinyl + Awesome Jeans`}, + + {`{% tablerow product in products cols:2 %}{{ product }}{% endtablerow %}`, + `Cool ShirtAlien Poster + Batman PosterBullseye Shirt + Another Classic VinylAwesome Jeans`}, } var iterationSyntaxErrorTests = []struct{ in, expected string }{ @@ -85,9 +101,12 @@ var iterationErrorTests = []struct{ in, expected string }{ var iterationTestBindings = map[string]interface{}{ "array": []string{"first", "second", "third"}, "hash": map[string]interface{}{"a": 1}, + "products": []string{ + "Cool Shirt", "Alien Poster", "Batman Poster", "Bullseye Shirt", "Another Classic Vinyl", "Awesome Jeans", + }, } -func TestLoopTag(t *testing.T) { +func TestIterationTags(t *testing.T) { config := render.NewConfig() AddStandardTags(config) for i, test := range iterationTests { @@ -97,7 +116,13 @@ func TestLoopTag(t *testing.T) { buf := new(bytes.Buffer) err = render.Render(root, buf, iterationTestBindings, config) require.NoErrorf(t, err, test.in) - require.Equalf(t, test.expected, buf.String(), test.in) + actual := buf.String() + if strings.Contains(test.in, "{% tablerow") { + replaceWS := regexp.MustCompile(`\n\s*`).ReplaceAllString + actual = replaceWS(actual, "") + test.expected = replaceWS(test.expected, "") + } + require.Equalf(t, test.expected, actual, test.in) }) } } diff --git a/tags/standard_tags.go b/tags/standard_tags.go index 6fee0c1..78a6980 100644 --- a/tags/standard_tags.go +++ b/tags/standard_tags.go @@ -25,7 +25,7 @@ func AddStandardTags(c render.Config) { c.AddBlock("for").Compiler(loopTagParser) c.AddBlock("if").Clause("else").Clause("elsif").Compiler(ifTagParser(true)) c.AddBlock("raw") - c.AddBlock("tablerow") + c.AddBlock("tablerow").Compiler(loopTagParser) c.AddBlock("unless").Compiler(ifTagParser(false)) }