1
0
mirror of https://github.com/danog/liquid.git synced 2024-11-30 05:58:59 +01:00

Move tags to own package

This commit is contained in:
Oliver Steele 2017-06-27 11:39:32 -04:00
parent 089a0c8125
commit 83503a149f
10 changed files with 156 additions and 93 deletions

View File

@ -38,6 +38,6 @@ type ASTObject struct {
type ASTControlTag struct {
Chunk
cd *ControlTagDefinition
body []ASTNode
branches []*ASTControlTag
Body []ASTNode
Branches []*ASTControlTag
}

View File

@ -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

View File

@ -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
}
}
}

View File

@ -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
}

View File

@ -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]

View File

@ -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)
}

View File

@ -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 }{

View File

@ -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"},

57
tags/tags.go Normal file
View File

@ -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
}
}
}

80
tags/tags_test.go Normal file
View File

@ -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)
})
}
}