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

Complete #4 case…else

This commit is contained in:
Oliver Steele 2017-07-12 09:18:49 -04:00
parent 5a12245d9b
commit 26bdd09897
4 changed files with 91 additions and 65 deletions

View File

@ -31,7 +31,7 @@ The [feature parity board](https://github.com/osteele/liquid/projects/1) lists d
In brief, these aren't implemented:
- The `cycle` and `tablerow` tags
- `{% case %}…{% else %}` and `{% when a or b %}`
- `{% when a or b %}`
- The `sort_natural`, `url_decode`, and `url_encode` filters
- Loop ranges `{% for a in 1...10 %}`
- Error modes

View File

@ -1,8 +1,8 @@
package render
import (
"fmt"
"io"
"sort"
"github.com/osteele/liquid/parser"
)
@ -14,20 +14,20 @@ type BlockCompiler func(BlockNode) (func(io.Writer, Context) error, error)
type blockSyntax struct {
name string
isClauseTag, isEndTag bool
syntaxModel *blockSyntax
parent *blockSyntax
startName string // for an end tag, the name of the correspondign start tag
parents map[string]bool // if non-nil, must be an immediate clause of one of these
parser BlockCompiler
}
func (s *blockSyntax) CanHaveParent(parent parser.BlockSyntax) bool {
if parent == nil {
return false
switch {
case s.isClauseTag:
return parent != nil && s.parents[parent.TagName()]
case s.isEndTag:
return parent != nil && parent.TagName() == s.startName
default:
return true
}
p := parent.(*blockSyntax)
if !s.isEndTag && p.syntaxModel != nil {
p = p.syntaxModel
}
return s.parent == p
}
func (s *blockSyntax) IsBlock() bool { return true }
@ -36,15 +36,19 @@ func (s *blockSyntax) IsBlockStart() bool { return !s.isClauseTag && !s.isEndT
func (s *blockSyntax) IsClause() bool { return s.isClauseTag }
func (s *blockSyntax) RequiresParent() bool { return s.isClauseTag || s.isEndTag }
func (s *blockSyntax) ParentTags() []string {
if s.parent == nil {
return []string{}
func (s *blockSyntax) ParentTags() (parents []string) {
for k := range s.parents {
parents = append(parents, k)
}
return []string{s.parent.name}
sort.Strings(parents)
return
}
func (s *blockSyntax) TagName() string { return s.name }
func (g grammar) addBlockDef(ct *blockSyntax) {
if g.blockDefs[ct.name] != nil {
panic("duplicate definition of " + ct.name)
}
g.blockDefs[ct.name] = ct
}
@ -68,26 +72,36 @@ type blockDefBuilder struct {
func (g grammar) AddBlock(name string) blockDefBuilder { // nolint: golint
ct := &blockSyntax{name: name}
g.addBlockDef(ct)
g.addBlockDef(&blockSyntax{name: "end" + name, isEndTag: true, parent: ct})
g.addBlockDef(&blockSyntax{name: "end" + name, isEndTag: true, startName: name})
return blockDefBuilder{g, ct}
}
// Clause tells the parser that the named tag can appear immediately between this tag and its end tag,
// so long as it is not nested within any other control tag.
func (b blockDefBuilder) Clause(name string) blockDefBuilder {
b.addBlockDef(&blockSyntax{name: name, isClauseTag: true, parent: b.tag})
if b.blockDefs[name] == nil {
b.addBlockDef(&blockSyntax{name: name, isClauseTag: true})
}
c := b.blockDefs[name]
if !c.isClauseTag {
panic(name + " has already been defined as a non-clause")
}
if len(c.parents) == 0 {
c.parents = make(map[string]bool)
}
c.parents[b.tag.name] = true
return b
}
// SameSyntaxAs tells the parser that this tag has the same syntax as the named tag.
func (b blockDefBuilder) SameSyntaxAs(name string) blockDefBuilder {
rt := b.blockDefs[name]
if rt == nil {
panic(fmt.Errorf("undefined: %s", name))
}
b.tag.syntaxModel = rt
return b
}
// func (b blockDefBuilder) SameSyntaxAs(name string) blockDefBuilder {
// rt := b.blockDefs[name]
// if rt == nil {
// panic(fmt.Errorf("undefined: %s", name))
// }
// b.tag.syntaxModel = rt
// return b
// }
// Compiler sets the parser for a control tag definition.
func (b blockDefBuilder) Compiler(fn BlockCompiler) {

View File

@ -20,7 +20,7 @@ func AddStandardTags(c render.Config) {
c.AddTag("break", breakTag)
c.AddTag("continue", continueTag)
c.AddBlock("capture").Compiler(captureTagParser)
c.AddBlock("case").Clause("when").Compiler(caseTagParser)
c.AddBlock("case").Clause("when").Clause("else").Compiler(caseTagParser)
c.AddBlock("comment")
c.AddBlock("for").Compiler(loopTagParser)
c.AddBlock("if").Clause("else").Clause("elsif").Compiler(ifTagParser(true))
@ -57,29 +57,40 @@ func caseTagParser(node render.BlockNode) (func(io.Writer, render.Context) error
return nil, err
}
type caseRec struct {
expr expression.Expression
// expr expression.Expression
test func(interface{}, render.Context) (bool, error)
node *render.BlockNode
}
cases := []caseRec{}
for _, clause := range node.Clauses {
bfn, err := expression.Parse(clause.Args)
if err != nil {
return nil, err
testFn := func(interface{}, render.Context) (bool, error) { return true, nil }
if clause.Token.Name == "when" {
clauseExpr, err := expression.Parse(clause.Args)
if err != nil {
return nil, err
}
testFn = func(sel interface{}, ctx render.Context) (bool, error) {
value, err := ctx.Evaluate(clauseExpr)
if err != nil {
return false, err
}
return evaluator.Equal(sel, value), nil
}
}
cases = append(cases, caseRec{bfn, clause})
cases = append(cases, caseRec{testFn, clause})
}
return func(w io.Writer, ctx render.Context) error {
value, err := ctx.Evaluate(expr)
sel, err := ctx.Evaluate(expr)
if err != nil {
return err
}
for _, branch := range cases {
b, err := ctx.Evaluate(branch.expr)
for _, clause := range cases {
f, err := clause.test(sel, ctx)
if err != nil {
return err
}
if evaluator.Equal(value, b) {
return ctx.RenderChild(w, branch.node)
if f {
return ctx.RenderChild(w, clause.node)
}
}
return nil

View File

@ -10,48 +10,49 @@ import (
)
var parseErrorTests = []struct{ in, expected string }{
{"{%unknown_tag%}", "unknown tag"},
{"{%if syntax error%}", "unterminated if block"},
{"{% unknown_tag %}", "unknown tag"},
{"{% if syntax error %}", "unterminated if block"},
// TODO once expression parsing is moved to template parse stage
// {"{%if syntax error%}{%endif%}", "parse error"},
// {"{%for a in ar unknown%}{{a}} {%endfor%}", "TODO"},
// {"{% if syntax error %}{% endif %}", "parse error"},
// {"{% for a in ar unknown %}{{ a }} {% endfor %}", "TODO"},
}
var tagTests = []struct{ in, expected string }{
// variables
{`{%assign av = 1%}{{av}}`, "1"},
{`{%assign av = obj.a%}{{av}}`, "1"},
{`{%capture x%}captured{%endcapture%}{{x}}`, "captured"},
{`{% assign av = 1 %}{{ av }}`, "1"},
{`{% assign av = obj.a %}{{ av }}`, "1"},
{`{% capture x %}captured{% endcapture %}{{ x }}`, "captured"},
// TODO test whether this requires matching interior tags
{`{%comment%}{{a}}{%unknown%}{%endcomment%}`, ""},
{`{% comment %}{{ a }}{% unknown %}{% endcomment %}`, ""},
// conditionals
{`{%case 1%}{%when 1%}a{%when 2%}b{%endcase%}`, "a"},
{`{%case 2%}{%when 1%}a{%when 2%}b{%endcase%}`, "b"},
{`{%case 3%}{%when 1%}a{%when 2%}b{%endcase%}`, ""},
// {`{%case 2%}{%when 1%}a{%else 2%}b{%endcase%}`, "captured"},
{`{% case 1 %}{% when 1 %}a{% when 2 %}b{% endcase %}`, "a"},
{`{% case 2 %}{% when 1 %}a{% when 2 %}b{% endcase %}`, "b"},
{`{% case 3 %}{% when 1 %}a{% when 2 %}b{% endcase %}`, ""},
{`{% case 1 %}{% when 1 %}a{% else %}b{% endcase %}`, "a"},
{`{% case 2 %}{% when 1 %}a{% else %}b{% endcase %}`, "b"},
{`{%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"},
{`{% 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{%endunless%}`, ""},
{`{%unless false%}true{%endunless%}`, "true"},
{`{% unless true %}false{% endunless %}`, ""},
{`{% unless false %}true{% endunless %}`, "true"},
// TODO test whether this requires matching interior tags
{`pre{%raw%}{{a}}{%unknown%}{%endraw%}post`, "pre{{a}}{%unknown%}post"},
{`pre{%raw%}{%if false%}anyway-{%endraw%}post`, "pre{%if false%}anyway-post"},
{`pre{% raw %}{{ a }}{% unknown %}{% endraw %}post`, "pre{{ a }}{% unknown %}post"},
{`pre{% raw %}{% if false %}anyway-{% endraw %}post`, "pre{% if false %}anyway-post"},
}
var tagTestBindings = map[string]interface{}{