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:
parent
089a0c8125
commit
83503a149f
@ -38,6 +38,6 @@ type ASTObject struct {
|
||||
type ASTControlTag struct {
|
||||
Chunk
|
||||
cd *ControlTagDefinition
|
||||
body []ASTNode
|
||||
branches []*ASTControlTag
|
||||
Body []ASTNode
|
||||
Branches []*ASTControlTag
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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]
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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 }{
|
||||
|
@ -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
57
tags/tags.go
Normal 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
80
tags/tags_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user