From 83503a149fc55247db6dd9c01346d3524aec85e7 Mon Sep 17 00:00:00 2001 From: Oliver Steele Date: Tue, 27 Jun 2017 11:39:32 -0400 Subject: [PATCH] Move tags to own package --- chunks/ast.go | 4 +-- chunks/context.go | 3 +- chunks/control_tags.go | 55 +---------------------------- chunks/marshal.go | 4 +-- chunks/parser.go | 6 ++-- chunks/render.go | 6 ++-- chunks/render_test.go | 29 +-------------- liquid_test.go | 5 +++ tags/tags.go | 57 ++++++++++++++++++++++++++++++ tags/tags_test.go | 80 ++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 156 insertions(+), 93 deletions(-) create mode 100644 tags/tags.go create mode 100644 tags/tags_test.go diff --git a/chunks/ast.go b/chunks/ast.go index 1153079..f8ee100 100644 --- a/chunks/ast.go +++ b/chunks/ast.go @@ -38,6 +38,6 @@ type ASTObject struct { type ASTControlTag struct { Chunk cd *ControlTagDefinition - body []ASTNode - branches []*ASTControlTag + Body []ASTNode + Branches []*ASTControlTag } diff --git a/chunks/context.go b/chunks/context.go index 6f2d3c8..5f717be 100644 --- a/chunks/context.go +++ b/chunks/context.go @@ -41,7 +41,8 @@ func (c *Context) evaluateStatement(tag, source string) (interface{}, error) { return c.EvaluateExpr(fmt.Sprintf("%%%s %s", tag, source)) } -func makeExpressionValueFn(source string) (func(Context) (interface{}, error), error) { +// MakeExpressionValueFn parses source into an evaluation function +func MakeExpressionValueFn(source string) (func(Context) (interface{}, error), error) { expr, err := expressions.Parse(source) if err != nil { return nil, err diff --git a/chunks/control_tags.go b/chunks/control_tags.go index 2ad7a74..7e902db 100644 --- a/chunks/control_tags.go +++ b/chunks/control_tags.go @@ -5,22 +5,11 @@ import ( "io" ) -func init() { - loopTags := []string{"break", "continue", "cycle"} - DefineControlTag("comment") //.Action(unimplementedControlTag) - DefineControlTag("if").Branch("else").Branch("elsif").Action(ifTagAction(true)) - DefineControlTag("unless").SameSyntaxAs("if").Action(ifTagAction(false)) - DefineControlTag("case").Branch("when") //.Action(unimplementedControlTag) - DefineControlTag("for").Governs(loopTags) //.Action(unimplementedControlTag) - DefineControlTag("tablerow").Governs(loopTags) //.Action(unimplementedControlTag) - DefineControlTag("capture") //.Action(unimplementedControlTag) -} - // ControlTagDefinitions is a map of tag names to control tag definitions. var ControlTagDefinitions = map[string]*ControlTagDefinition{} // ControlTagAction runs the interpreter. -type ControlTagAction func(*ASTControlTag) func(io.Writer, Context) error +type ControlTagAction func(ASTControlTag) func(io.Writer, Context) error // ControlTagDefinition tells the parser how to parse control tags. type ControlTagDefinition struct { @@ -93,45 +82,3 @@ func (ct *ControlTagDefinition) SameSyntaxAs(name string) *ControlTagDefinition func (ct *ControlTagDefinition) Action(fn ControlTagAction) { ct.action = fn } - -// func unimplementedControlTag(io.Writer, Context) error { -// return fmt.Errorf("unimplemented control tag") -// } - -func ifTagAction(polarity bool) func(*ASTControlTag) func(io.Writer, Context) error { - return func(n *ASTControlTag) func(io.Writer, Context) error { - expr, err := makeExpressionValueFn(n.Args) - if err != nil { - return func(io.Writer, Context) error { return err } - } - return func(w io.Writer, ctx Context) error { - val, err := expr(ctx) - if err != nil { - return err - } - if !polarity { - val = (val == nil || val == false) - } - switch val { - default: - return renderASTSequence(w, n.body, ctx) - case nil, false: - for _, c := range n.branches { - switch c.Tag { - case "else": - val = true - case "elsif": - val, err = ctx.EvaluateExpr(c.Args) - if err != nil { - return err - } - } - if val != nil && val != false { - return renderASTSequence(w, c.body, ctx) - } - } - } - return nil - } - } -} diff --git a/chunks/marshal.go b/chunks/marshal.go index 0773594..8496d17 100644 --- a/chunks/marshal.go +++ b/chunks/marshal.go @@ -39,8 +39,8 @@ func (n ASTControlTag) MarshalYAML() (interface{}, error) { return map[string]map[string]interface{}{ n.cd.Name: { "args": n.Args, - "body": n.body, - "branches": n.branches, + "body": n.Body, + "branches": n.Branches, }}, nil } diff --git a/chunks/parser.go b/chunks/parser.go index 4fcfa02..be1366d 100644 --- a/chunks/parser.go +++ b/chunks/parser.go @@ -37,11 +37,11 @@ func Parse(chunks []Chunk) (ASTNode, error) { stack = append(stack, frame{cd: ccd, cn: ccn, ap: ap}) ccd, ccn = cd, &ASTControlTag{Chunk: c, cd: cd} *ap = append(*ap, ccn) - ap = &ccn.body + ap = &ccn.Body case cd.IsBranchTag: n := &ASTControlTag{Chunk: c, cd: cd} - ccn.branches = append(ccn.branches, n) - ap = &n.body + ccn.Branches = append(ccn.Branches, n) + ap = &n.Body case cd.IsEndTag: f := stack[len(stack)-1] ccd, ccn, ap, stack = f.cd, f.cn, f.ap, stack[:len(stack)-1] diff --git a/chunks/render.go b/chunks/render.go index 13d1935..90e7cb4 100644 --- a/chunks/render.go +++ b/chunks/render.go @@ -33,8 +33,8 @@ func (n *ASTText) Render(w io.Writer, _ Context) error { return err } -// Render evaluates an AST node and writes the result to an io.Writer. -func renderASTSequence(w io.Writer, seq []ASTNode, ctx Context) error { +// RenderASTSequence renders a sequence of nodes. +func (ctx Context) RenderASTSequence(w io.Writer, seq []ASTNode) error { for _, n := range seq { if err := n.Render(w, ctx); err != nil { return err @@ -49,7 +49,7 @@ func (n *ASTControlTag) Render(w io.Writer, ctx Context) error { if !ok { return fmt.Errorf("unimplemented tag: %s", n.Tag) } - f := cd.action(n) + f := cd.action(*n) return f(w, ctx) } diff --git a/chunks/render_test.go b/chunks/render_test.go index d46cbc2..360076d 100644 --- a/chunks/render_test.go +++ b/chunks/render_test.go @@ -10,7 +10,7 @@ import ( var parseErrorTests = []struct{ in, expected string }{ {"{%unknown_tag%}", "unknown tag"}, - {"{%if syntax error%}", "unterminated if tag"}, + // {"{%if syntax error%}", "unterminated if tag"}, // {"{%if syntax error%}{%endif%}", "parse error"}, } @@ -20,33 +20,6 @@ var renderTests = []struct{ in, expected string }{ {"{{x}}", "123"}, {"{{page.title}}", "Introduction"}, {"{{ar[1]}}", "second"}, - - {"{%if true%}true{%endif%}", "true"}, - {"{%if false%}false{%endif%}", ""}, - {"{%if 0%}true{%endif%}", "true"}, - {"{%if 1%}true{%endif%}", "true"}, - {"{%if x%}true{%endif%}", "true"}, - {"{%if y%}true{%endif%}", ""}, - {"{%if true%}true{%endif%}", "true"}, - {"{%if false%}false{%endif%}", ""}, - {"{%if true%}true{%else%}false{%endif%}", "true"}, - {"{%if false%}false{%else%}true{%endif%}", "true"}, - {"{%if true%}0{%elsif true%}1{%else%}2{%endif%}", "0"}, - {"{%if false%}0{%elsif true%}1{%else%}2{%endif%}", "1"}, - {"{%if false%}0{%elsif false%}1{%else%}2{%endif%}", "2"}, - - {"{%unless true%}false{%endif%}", ""}, - {"{%unless false%}true{%endif%}", "true"}, - {"{%unless true%}false{%else%}true{%endif%}", "true"}, - {"{%unless false%}true{%else%}false{%endif%}", "true"}, - {"{%unless false%}0{%elsif true%}1{%else%}2{%endif%}", "0"}, - {"{%unless true%}0{%elsif true%}1{%else%}2{%endif%}", "1"}, - {"{%unless true%}0{%elsif false%}1{%else%}2{%endif%}", "2"}, - - {"{%assign av = 1%}{{av}}", "1"}, - {"{%assign av = obj.a%}{{av}}", "1"}, - - // {"{%for a in ar%}{{a}} {{%endfor%}", "first second third "}, } var filterTests = []struct{ in, expected string }{ diff --git a/liquid_test.go b/liquid_test.go index 3e5a70a..c29ef94 100644 --- a/liquid_test.go +++ b/liquid_test.go @@ -5,9 +5,14 @@ import ( "log" "testing" + "github.com/osteele/liquid/tags" "github.com/stretchr/testify/require" ) +func init() { + tags.DefineStandardTags() +} + var liquidTests = []struct{ in, expected string }{ {"{{page.title}}", "Introduction"}, {"{%if x%}true{%endif%}", "true"}, diff --git a/tags/tags.go b/tags/tags.go new file mode 100644 index 0000000..5c2e84c --- /dev/null +++ b/tags/tags.go @@ -0,0 +1,57 @@ +package tags + +import ( + "io" + + "github.com/osteele/liquid/chunks" +) + +func DefineStandardTags() { + loopTags := []string{"break", "continue", "cycle"} + chunks.DefineControlTag("comment") + chunks.DefineControlTag("if").Branch("else").Branch("elsif").Action(ifTagAction(true)) + chunks.DefineControlTag("unless").SameSyntaxAs("if").Action(ifTagAction(false)) + chunks.DefineControlTag("case").Branch("when") + chunks.DefineControlTag("for").Governs(loopTags) + chunks.DefineControlTag("tablerow").Governs(loopTags) + chunks.DefineControlTag("capture") +} + + +func ifTagAction(polarity bool) func(chunks.ASTControlTag) func(io.Writer, chunks.Context) error { + return func(node chunks.ASTControlTag) func(io.Writer, chunks.Context) error { + expr, err := chunks.MakeExpressionValueFn(node.Args) + if err != nil { + return func(io.Writer, chunks.Context) error { return err } + } + return func(w io.Writer, ctx chunks.Context) error { + val, err := expr(ctx) + if err != nil { + return err + } + if !polarity { + val = (val == nil || val == false) + } + switch val { + default: + return ctx.RenderASTSequence(w, node.Body) + case nil, false: + for _, c := range node.Branches { + switch c.Tag { + case "else": + val = true + case "elsif": + val, err = ctx.EvaluateExpr(c.Args) + if err != nil { + return err + } + } + if val != nil && val != false { + return ctx.RenderASTSequence(w, c.Body) + } + } + } + return nil + } + } +} diff --git a/tags/tags_test.go b/tags/tags_test.go new file mode 100644 index 0000000..fd04afa --- /dev/null +++ b/tags/tags_test.go @@ -0,0 +1,80 @@ +package tags + +import ( + "bytes" + "fmt" + "testing" + + "github.com/osteele/liquid/chunks" + "github.com/stretchr/testify/require" +) + +var parseErrorTests = []struct{ in, expected string }{ + {"{%unknown_tag%}", "unknown tag"}, + {"{%if syntax error%}", "unterminated if tag"}, + // {"{%if syntax error%}{%endif%}", "parse error"}, +} + +var renderTests = []struct{ in, expected string }{ + {"{{12}}", "12"}, + {"{{x}}", "123"}, + {"{{page.title}}", "Introduction"}, + {"{{ar[1]}}", "second"}, +} + +var renderTestContext = chunks.NewContext(map[string]interface{}{ + "x": 123, + "obj": map[string]interface{}{ + "a": 1, + }, + "animals": []string{"zebra", "octopus", "giraffe", "Sally Snake"}, + "pages": []map[string]interface{}{ + {"category": "business"}, + {"category": "celebrities"}, + {}, + {"category": "lifestyle"}, + {"category": "sports"}, + {}, + {"category": "technology"}, + }, + "sort_prop": []map[string]interface{}{ + {"weight": 1}, + {"weight": 5}, + {"weight": 3}, + {"weight": nil}, + }, + "ar": []string{"first", "second", "third"}, + "page": map[string]interface{}{ + "title": "Introduction", + }, +}) + +func init() { + DefineStandardTags() +} +func TestParseErrors(t *testing.T) { + for i, test := range parseErrorTests { + t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) { + tokens := chunks.Scan(test.in, "") + ast, err := chunks.Parse(tokens) + require.Nilf(t, ast, test.in) + require.Errorf(t, err, test.in) + require.Containsf(t, err.Error(), test.expected, test.in) + }) + } +} +func TestRender(t *testing.T) { + for i, test := range renderTests { + t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) { + tokens := chunks.Scan(test.in, "") + // fmt.Println(tokens) + ast, err := chunks.Parse(tokens) + require.NoErrorf(t, err, test.in) + // fmt.Println(MustYAML(ast)) + buf := new(bytes.Buffer) + err = ast.Render(buf, renderTestContext) + require.NoErrorf(t, err, test.in) + require.Equalf(t, test.expected, buf.String(), test.in) + }) + } +}