1
0
mirror of https://github.com/danog/liquid.git synced 2024-11-30 06:08:57 +01:00

Split package render->parser

This commit is contained in:
Oliver Steele 2017-07-07 05:41:37 -04:00
parent 18e2540b3a
commit 903acb8d2e
26 changed files with 174 additions and 126 deletions

View File

@ -1,9 +1,6 @@
# Go Liquid Template Parser
[![Build Status](https://travis-ci.org/osteele/liquid.svg?branch=master)](https://travis-ci.org/osteele/liquid)
[![Go Report Card](https://goreportcard.com/badge/github.com/osteele/liquid)](https://goreportcard.com/report/github.com/osteele/liquid)
[![GoDoc](https://godoc.org/github.com/osteele/liquid?status.svg)](http://godoc.org/github.com/osteele/liquid)
[![Coverage Status](https://coveralls.io/repos/github/osteele/liquid/badge.svg?branch=master)](https://coveralls.io/github/osteele/liquid?branch=master)
[![][travis-svg]][travis-url] [![][coveralls-svg]][coveralls-url] [![][go-report-card-svg]][go-report-card-url] [![][godoc-svg]][godoc-url] [![][license-svg]][license-url]
> “Any sufficiently complicated C or Fortran program contains an ad-hoc, informally-specified, bug-ridden, slow implementation of half of Common Lisp.” Philip Greenspun
@ -127,3 +124,18 @@ The [original Liquid engine](https://shopify.github.io/liquid), of course, for t
## License
MIT License
[coveralls-url]: https://coveralls.io/r/osteele/liquid?branch=master
[coveralls-svg]: https://img.shields.io/coveralls/osteele/liquid.svg?branch=master
[godoc-url]: https://godoc.org/github.com/osteele/liquid
[godoc-svg]: https://godoc.org/github.com/osteele/liquid?status.svg
[license-url]: https://github.com/osteele/liquid/blob/master/LICENSE
[license-svg]: https://img.shields.io/badge/license-MIT-blue.svg
[go-report-card-url]: https://goreportcard.com/report/github.com/osteele/liquid
[go-report-card-svg]: https://goreportcard.com/badge/github.com/osteele/liquid
[travis-url]: https://travis-ci.org/osteele/liquid
[travis-svg]: https://img.shields.io/travis/osteele/liquid.svg?branch=master

View File

@ -13,7 +13,7 @@ type engine struct{ settings render.Config }
// NewEngine returns a new template engine.
func NewEngine() Engine {
e := engine{render.NewConfig()}
filters.AddStandardFilters(e.settings.Config)
filters.AddStandardFilters(&e.settings.Config.Config)
tags.AddStandardTags(e.settings)
return e
}
@ -57,7 +57,7 @@ func (e engine) ParseTemplate(text []byte) (Template, error) {
if err != nil {
return nil, err
}
return &template{ast, e.settings}, nil
return &template{ast, &e.settings}, nil
}
// ParseAndRender is in the Engine interface.

View File

@ -1,7 +1,6 @@
package evaluator
import (
"fmt"
"reflect"
)
@ -15,7 +14,6 @@ func Call(fn reflect.Value, args []interface{}) (interface{}, error) {
if len(outs) > 1 && outs[1].Interface() != nil {
switch e := outs[1].Interface().(type) {
case error:
fmt.Println("error")
return nil, e
default:
panic(e)

View File

@ -44,6 +44,9 @@ func (d *filterDictionary) AddFilter(name string, fn interface{}) {
// case rf.Type().Out(1).Implements(…):
// panic(typeError("a filter's second output must be type error"))
}
if len(d.filters) == 0 {
d.filters = make(map[string]interface{})
}
d.filters[name] = fn
}

View File

@ -9,7 +9,6 @@ import (
func TestContext_runFilter(t *testing.T) {
cfg := NewConfig()
ctx := NewContext(map[string]interface{}{"x": 10}, cfg)
constant := func(value interface{}) valueFn {
return func(Context) interface{} { return value }
}
@ -19,6 +18,7 @@ func TestContext_runFilter(t *testing.T) {
cfg.AddFilter("f1", func(s string) string {
return "<" + s + ">"
})
ctx := NewContext(map[string]interface{}{"x": 10}, cfg)
out := ctx.ApplyFilter("f1", receiver, []valueFn{})
require.Equal(t, "<self>", out)
@ -26,6 +26,7 @@ func TestContext_runFilter(t *testing.T) {
cfg.AddFilter("with_arg", func(a, b string) string {
return fmt.Sprintf("(%s, %s)", a, b)
})
ctx = NewContext(map[string]interface{}{"x": 10}, cfg)
out = ctx.ApplyFilter("with_arg", receiver, []valueFn{constant("arg")})
require.Equal(t, "(self, arg)", out)
@ -43,6 +44,7 @@ func TestContext_runFilter(t *testing.T) {
}
return fmt.Sprintf("(%v, %v)", a, value), nil
})
ctx = NewContext(map[string]interface{}{"x": 10}, cfg)
out = ctx.ApplyFilter("closure", receiver, []valueFn{constant("x |add: y")})
require.Equal(t, "(self, 11)", out)
}

View File

@ -161,7 +161,7 @@ var filterTestBindings = map[string]interface{}{
func TestFilters(t *testing.T) {
settings := expression.NewConfig()
AddStandardFilters(settings)
AddStandardFilters(&settings)
context := expression.NewContext(filterTestBindings, settings)
for i, test := range filterTests {

View File

@ -19,9 +19,9 @@ import (
)
// AddStandardFilters defines the standard Liquid filters.
func AddStandardFilters(settings expression.Config) { // nolint: gocyclo
func AddStandardFilters(cfg *expression.Config) { // nolint: gocyclo
// values
settings.AddFilter("default", func(value, defaultValue interface{}) interface{} {
cfg.AddFilter("default", func(value, defaultValue interface{}) interface{} {
if value == nil || value == false || evaluator.IsEmpty(value) {
value = defaultValue
}
@ -29,7 +29,7 @@ func AddStandardFilters(settings expression.Config) { // nolint: gocyclo
})
// dates
settings.AddFilter("date", func(t time.Time, format interface{}) (string, error) {
cfg.AddFilter("date", func(t time.Time, format interface{}) (string, error) {
form, ok := format.(string)
if !ok {
form = "%a, %b %d, %y"
@ -40,7 +40,7 @@ func AddStandardFilters(settings expression.Config) { // nolint: gocyclo
})
// arrays
settings.AddFilter("compact", func(array []interface{}) interface{} {
cfg.AddFilter("compact", func(array []interface{}) interface{} {
out := []interface{}{}
for _, item := range array {
if item != nil {
@ -49,25 +49,25 @@ func AddStandardFilters(settings expression.Config) { // nolint: gocyclo
}
return out
})
settings.AddFilter("join", joinFilter)
settings.AddFilter("map", func(array []map[string]interface{}, key string) interface{} {
cfg.AddFilter("join", joinFilter)
cfg.AddFilter("map", func(array []map[string]interface{}, key string) interface{} {
out := []interface{}{}
for _, obj := range array {
out = append(out, obj[key])
}
return out
})
settings.AddFilter("reverse", reverseFilter)
settings.AddFilter("sort", sortFilter)
cfg.AddFilter("reverse", reverseFilter)
cfg.AddFilter("sort", sortFilter)
// https://shopify.github.io/liquid/ does not demonstrate first and last as filters,
// but https://help.shopify.com/themes/liquid/filters/array-filters does
settings.AddFilter("first", func(array []interface{}) interface{} {
cfg.AddFilter("first", func(array []interface{}) interface{} {
if len(array) == 0 {
return nil
}
return array[0]
})
settings.AddFilter("last", func(array []interface{}) interface{} {
cfg.AddFilter("last", func(array []interface{}) interface{} {
if len(array) == 0 {
return nil
}
@ -75,20 +75,20 @@ func AddStandardFilters(settings expression.Config) { // nolint: gocyclo
})
// numbers
settings.AddFilter("abs", math.Abs)
settings.AddFilter("ceil", math.Ceil)
settings.AddFilter("floor", math.Floor)
settings.AddFilter("modulo", math.Mod)
settings.AddFilter("minus", func(a, b float64) float64 {
cfg.AddFilter("abs", math.Abs)
cfg.AddFilter("ceil", math.Ceil)
cfg.AddFilter("floor", math.Floor)
cfg.AddFilter("modulo", math.Mod)
cfg.AddFilter("minus", func(a, b float64) float64 {
return a - b
})
settings.AddFilter("plus", func(a, b float64) float64 {
cfg.AddFilter("plus", func(a, b float64) float64 {
return a + b
})
settings.AddFilter("times", func(a, b float64) float64 {
cfg.AddFilter("times", func(a, b float64) float64 {
return a * b
})
settings.AddFilter("divided_by", func(a float64, b interface{}) interface{} {
cfg.AddFilter("divided_by", func(a float64, b interface{}) interface{} {
switch bt := b.(type) {
case int, int16, int32, int64:
return int(a) / bt.(int)
@ -98,7 +98,7 @@ func AddStandardFilters(settings expression.Config) { // nolint: gocyclo
return nil
}
})
settings.AddFilter("round", func(n float64, places interface{}) float64 {
cfg.AddFilter("round", func(n float64, places interface{}) float64 {
pl, ok := places.(int)
if !ok {
pl = 0
@ -108,45 +108,45 @@ func AddStandardFilters(settings expression.Config) { // nolint: gocyclo
})
// sequences
settings.AddFilter("size", evaluator.Length)
cfg.AddFilter("size", evaluator.Length)
// strings
settings.AddFilter("append", func(s, suffix string) string {
cfg.AddFilter("append", func(s, suffix string) string {
return s + suffix
})
settings.AddFilter("capitalize", func(s, suffix string) string {
cfg.AddFilter("capitalize", func(s, suffix string) string {
if len(s) < 1 {
return s
}
return strings.ToUpper(s[:1]) + s[1:]
})
settings.AddFilter("downcase", func(s, suffix string) string {
cfg.AddFilter("downcase", func(s, suffix string) string {
return strings.ToLower(s)
})
settings.AddFilter("escape", html.EscapeString)
settings.AddFilter("escape_once", func(s, suffix string) string {
cfg.AddFilter("escape", html.EscapeString)
cfg.AddFilter("escape_once", func(s, suffix string) string {
return html.EscapeString(html.UnescapeString(s))
})
// TODO test case for this
settings.AddFilter("newline_to_br", func(s string) string {
cfg.AddFilter("newline_to_br", func(s string) string {
return strings.Replace(s, "\n", "<br />", -1)
})
settings.AddFilter("prepend", func(s, prefix string) string {
cfg.AddFilter("prepend", func(s, prefix string) string {
return prefix + s
})
settings.AddFilter("remove", func(s, old string) string {
cfg.AddFilter("remove", func(s, old string) string {
return strings.Replace(s, old, "", -1)
})
settings.AddFilter("remove_first", func(s, old string) string {
cfg.AddFilter("remove_first", func(s, old string) string {
return strings.Replace(s, old, "", 1)
})
settings.AddFilter("replace", func(s, old, new string) string {
cfg.AddFilter("replace", func(s, old, new string) string {
return strings.Replace(s, old, new, -1)
})
settings.AddFilter("replace_first", func(s, old, new string) string {
cfg.AddFilter("replace_first", func(s, old, new string) string {
return strings.Replace(s, old, new, 1)
})
settings.AddFilter("slice", func(s string, start int, length interface{}) string {
cfg.AddFilter("slice", func(s string, start int, length interface{}) string {
// runes aren't bytes; don't use slice
n, ok := length.(int)
if !ok {
@ -158,23 +158,23 @@ func AddStandardFilters(settings expression.Config) { // nolint: gocyclo
p := regexp.MustCompile(fmt.Sprintf(`^.{%d}(.{0,%d}).*$`, start, n))
return p.ReplaceAllString(s, "$1")
})
settings.AddFilter("split", splitFilter)
settings.AddFilter("strip_html", func(s string) string {
cfg.AddFilter("split", splitFilter)
cfg.AddFilter("strip_html", func(s string) string {
// TODO this probably isn't sufficient
return regexp.MustCompile(`<.*?>`).ReplaceAllString(s, "")
})
// TODO test case for this
settings.AddFilter("strip_newlines", func(s string) string {
cfg.AddFilter("strip_newlines", func(s string) string {
return strings.Replace(s, "\n", "", -1)
})
settings.AddFilter("strip", strings.TrimSpace)
settings.AddFilter("lstrip", func(s string) string {
cfg.AddFilter("strip", strings.TrimSpace)
cfg.AddFilter("lstrip", func(s string) string {
return strings.TrimLeftFunc(s, unicode.IsSpace)
})
settings.AddFilter("rstrip", func(s string) string {
cfg.AddFilter("rstrip", func(s string) string {
return strings.TrimRightFunc(s, unicode.IsSpace)
})
settings.AddFilter("truncate", func(s string, n int, ellipsis interface{}) string {
cfg.AddFilter("truncate", func(s string, n int, ellipsis interface{}) string {
// runes aren't bytes; don't use slice
el, ok := ellipsis.(string)
if !ok {
@ -183,20 +183,20 @@ func AddStandardFilters(settings expression.Config) { // nolint: gocyclo
p := regexp.MustCompile(fmt.Sprintf(`^(.{%d})..{%d,}`, n-len(el), len(el)))
return p.ReplaceAllString(s, `$1`+el)
})
settings.AddFilter("upcase", func(s, suffix string) string {
cfg.AddFilter("upcase", func(s, suffix string) string {
return strings.ToUpper(s)
})
// debugging extensions
// inspect is from Jekyll
settings.AddFilter("inspect", func(value interface{}) string {
cfg.AddFilter("inspect", func(value interface{}) string {
s, err := json.Marshal(value)
if err != nil {
return fmt.Sprintf("%#v", value)
}
return string(s)
})
settings.AddFilter("type", func(value interface{}) string {
cfg.AddFilter("type", func(value interface{}) string {
return reflect.TypeOf(value).String()
})
}

View File

@ -3,7 +3,7 @@ Package liquid is a pure Go implementation of Shopify Liquid templates, for use
See the project README https://github.com/osteele/liquid for additional information and implementation status.
Note that the API for this package is not frozen. It is *especiallY* likely that subpackage APIs will
Note that the API for this package is not frozen. It is *especially* likely that subpackage APIs will
change drastically. Don't use anything except from a subpackage except render.Context.
*/
package liquid
@ -11,6 +11,7 @@ package liquid
import (
"github.com/osteele/liquid/evaluator"
"github.com/osteele/liquid/expression"
"github.com/osteele/liquid/parser"
"github.com/osteele/liquid/render"
)
@ -71,9 +72,9 @@ func IsTemplateError(err error) bool {
return true
case expression.ParseError:
return true
case render.CompilationError:
case parser.ParseError:
return true
case render.ParseError:
case render.CompilationError:
return true
case render.Error:
return true

View File

@ -1,7 +1,6 @@
package liquid
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
@ -9,7 +8,6 @@ import (
func TestIsTemplateError(t *testing.T) {
_, err := NewEngine().ParseAndRenderString("{{ syntax error }}", emptyBindings)
fmt.Printf("%T", err)
require.True(t, IsTemplateError(err))
_, err = NewEngine().ParseAndRenderString("{% if %}", emptyBindings)
require.True(t, IsTemplateError(err))

View File

@ -1,4 +1,4 @@
package render
package parser
import (
"github.com/osteele/liquid/expression"
@ -17,7 +17,7 @@ type ASTBlock struct {
// ASTRaw holds the text between the start and end of a raw tag.
type ASTRaw struct {
slices []string
Slices []string
}
// ASTTag is a tag.
@ -33,7 +33,7 @@ type ASTText struct {
// ASTObject is an {{ object }} object.
type ASTObject struct {
Chunk
expr expression.Expression
Expr expression.Expression
}
// ASTSeq is a sequence of nodes.

View File

@ -1,4 +1,4 @@
package render
package parser
import "fmt"

View File

@ -1,6 +1,6 @@
// Code generated by "stringer -type=ChunkType"; DO NOT EDIT.
package render
package parser
import "fmt"

15
parser/config.go Normal file
View File

@ -0,0 +1,15 @@
package parser
import "github.com/osteele/liquid/expression"
// // Config holds configuration information for parsing and rendering.
type Config struct {
expression.Config
// Filename string
Grammar Grammar
}
// NewConfig creates a new Settings.
func NewConfig() Config {
return Config{Config: expression.NewConfig()}
}

View File

@ -1,4 +1,4 @@
package render
package parser
// Grammar supplies the parser with syntax information about blocks.
type Grammar interface {
@ -18,4 +18,4 @@ type BlockSyntax interface {
}
// Grammar returns a configuration's grammar.
func (c *Config) Grammar() Grammar { return c }
// func (c *Config) Grammar() Grammar { return c }

View File

@ -1,4 +1,4 @@
package render
package parser
import (
"fmt"
@ -31,7 +31,7 @@ func (c Config) parseChunks(chunks []Chunk) (ASTNode, error) { // nolint: gocycl
ap *[]ASTNode
}
var (
g = c.Grammar()
g = c.Grammar
root = &ASTSeq{} // root of AST; will be returned
ap = &root.Children // newly-constructed nodes are appended here
sd BlockSyntax // current block syntax definition
@ -54,7 +54,7 @@ func (c Config) parseChunks(chunks []Chunk) (ASTNode, error) { // nolint: gocycl
if ch.Type == TagChunkType && ch.Name == "endraw" {
inRaw = false
} else {
rawTag.slices = append(rawTag.slices, ch.Source)
rawTag.Slices = append(rawTag.Slices, ch.Source)
}
case ch.Type == ObjChunkType:
expr, err := expression.Parse(ch.Args)
@ -99,7 +99,7 @@ func (c Config) parseChunks(chunks []Chunk) (ASTNode, error) { // nolint: gocycl
}
pop()
default:
panic("unexpected block type")
panic(fmt.Errorf("block type %q", ch.Name))
}
} else {
*ap = append(*ap, &ASTTag{ch})

View File

@ -1,21 +1,33 @@
package render
package parser
import (
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/require"
)
func addParserTestTags(s Config) {
s.AddBlock("case").Branch("when")
s.AddBlock("comment")
s.AddBlock("for").Governs([]string{"break"})
s.AddBlock("if").Branch("else").Branch("elsif")
s.AddBlock("unless").SameSyntaxAs("if")
s.AddBlock("raw")
type grammarFake struct{}
type blockSyntaxFake string
func (g grammarFake) BlockSyntax(w string) (BlockSyntax, bool) {
return blockSyntaxFake(w), true
}
func (g blockSyntaxFake) IsBlock() bool { return true }
func (g blockSyntaxFake) CanHaveParent(p BlockSyntax) bool {
return string(g) == "end"+p.TagName() || (g == "else" && p.TagName() == "if")
}
func (g blockSyntaxFake) IsBlockEnd() bool { return strings.HasPrefix(string(g), "end") }
func (g blockSyntaxFake) IsBlockStart() bool {
return g == "for" || g == "if" || g == "unless"
}
func (g blockSyntaxFake) IsBranch() bool { return g == "else" }
func (g blockSyntaxFake) ParentTags() []string { return []string{"unless"} }
func (g blockSyntaxFake) RequiresParent() bool { return g == "else" || g.IsBlockEnd() }
func (g blockSyntaxFake) TagName() string { return string(g) }
var parseErrorTests = []struct{ in, expected string }{
{"{% if test %}", "unterminated if block"},
{"{% if test %}{% endunless %}", "not inside unless"},
@ -28,7 +40,7 @@ var parserTests = []struct{ in string }{
{`{% for item in list %}{% endfor %}`},
{`{% if test %}{% else %}{% endif %}`},
{`{% if test %}{% if test %}{% endif %}{% endif %}`},
{`{% unless test %}{% else %}{% endunless %}`},
{`{% unless test %}{% endunless %}`},
{`{% for item in list %}{% if test %}{% else %}{% endif %}{% endfor %}`},
{`{% if true %}{% raw %}{% endraw %}{% endif %}`},
@ -37,11 +49,10 @@ var parserTests = []struct{ in string }{
}
func TestParseErrors(t *testing.T) {
settings := NewConfig()
addParserTestTags(settings)
cfg := Config{Grammar:grammarFake{}}
for i, test := range parseErrorTests {
t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) {
_, err := settings.Parse(test.in)
_, err := cfg.Parse(test.in)
require.Errorf(t, err, test.in)
require.Containsf(t, err.Error(), test.expected, test.in)
})
@ -49,11 +60,10 @@ func TestParseErrors(t *testing.T) {
}
func TestParser(t *testing.T) {
settings := NewConfig()
addParserTestTags(settings)
cfg := Config{Grammar:grammarFake{}}
for i, test := range parserTests {
t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) {
_, err := settings.Parse(test.in)
_, err := cfg.Parse(test.in)
require.NoError(t, err, test.in)
})
}

View File

@ -1,4 +1,4 @@
package render
package parser
import (
"regexp"

View File

@ -1,4 +1,4 @@
package render
package parser
import (
"fmt"

View File

@ -3,6 +3,8 @@ package render
import (
"fmt"
"io"
"github.com/osteele/liquid/parser"
)
// BlockCompiler builds a renderer for the tag instance.
@ -17,7 +19,7 @@ type blockSyntax struct {
parser BlockCompiler
}
func (c *blockSyntax) CanHaveParent(parent BlockSyntax) bool {
func (c *blockSyntax) CanHaveParent(parent parser.BlockSyntax) bool {
if parent == nil {
return false
}
@ -52,7 +54,7 @@ func (c Config) findBlockDef(name string) (*blockSyntax, bool) {
}
// BlockSyntax is part of the Grammar interface.
func (c Config) BlockSyntax(name string) (BlockSyntax, bool) {
func (c Config) BlockSyntax(name string) (parser.BlockSyntax, bool) {
ct, found := c.blockDefs[name]
return ct, found
}

View File

@ -2,6 +2,8 @@ package render
import (
"fmt"
"github.com/osteele/liquid/parser"
)
// A CompilationError is a parse error during template compilation.
@ -14,7 +16,7 @@ func compilationErrorf(format string, a ...interface{}) CompilationError {
}
// Compile parses a source template. It returns an AST root, that can be evaluated.
func (c Config) Compile(source string) (ASTNode, error) {
func (c Config) Compile(source string) (parser.ASTNode, error) {
root, err := c.Parse(source)
if err != nil {
return nil, err
@ -23,9 +25,9 @@ func (c Config) Compile(source string) (ASTNode, error) {
}
// nolint: gocyclo
func (c Config) compileNode(n ASTNode) (Node, error) {
func (c Config) compileNode(n parser.ASTNode) (Node, error) {
switch n := n.(type) {
case *ASTBlock:
case *parser.ASTBlock:
body, err := c.compileNodes(n.Body)
if err != nil {
return nil, err
@ -52,15 +54,15 @@ func (c Config) compileNode(n ASTNode) (Node, error) {
node.renderer = r
}
return &node, nil
case *ASTRaw:
return &RawNode{n.slices}, nil
case *ASTSeq:
case *parser.ASTRaw:
return &RawNode{n.Slices}, nil
case *parser.ASTSeq:
children, err := c.compileNodes(n.Children)
if err != nil {
return nil, err
}
return &SeqNode{children}, nil
case *ASTTag:
case *parser.ASTTag:
if td, ok := c.FindTagDefinition(n.Name); ok {
f, err := td(n.Args)
if err != nil {
@ -69,16 +71,16 @@ func (c Config) compileNode(n ASTNode) (Node, error) {
return &TagNode{n.Chunk, f}, nil
}
return nil, compilationErrorf("unknown tag: %s", n.Name)
case *ASTText:
case *parser.ASTText:
return &TextNode{n.Chunk}, nil
case *ASTObject:
return &ObjectNode{n.Chunk, n.expr}, nil
case *parser.ASTObject:
return &ObjectNode{n.Chunk, n.Expr}, nil
default:
panic(fmt.Errorf("un-compilable node type %T", n))
}
}
func (c Config) compileBlocks(blocks []*ASTBlock) ([]*BlockNode, error) {
func (c Config) compileBlocks(blocks []*parser.ASTBlock) ([]*BlockNode, error) {
out := make([]*BlockNode, 0, len(blocks))
for _, child := range blocks {
compiled, err := c.compileNode(child)
@ -90,7 +92,7 @@ func (c Config) compileBlocks(blocks []*ASTBlock) ([]*BlockNode, error) {
return out, nil
}
func (c Config) compileNodes(nodes []ASTNode) ([]Node, error) {
func (c Config) compileNodes(nodes []parser.ASTNode) ([]Node, error) {
out := make([]Node, 0, len(nodes))
for _, child := range nodes {
compiled, err := c.compileNode(child)

View File

@ -1,11 +1,12 @@
package render
import "github.com/osteele/liquid/expression"
import (
"github.com/osteele/liquid/parser"
)
// Config holds configuration information for parsing and rendering.
type Config struct {
// ExpressionConfig expression.Config
expression.Config
parser.Config
Filename string
tags map[string]TagCompiler
blockDefs map[string]*blockSyntax
@ -13,15 +14,16 @@ type Config struct {
// NewConfig creates a new Settings.
func NewConfig() Config {
s := Config{
Config: expression.NewConfig(),
c := Config{
// Config: parser.NewConfig(),
tags: map[string]TagCompiler{},
blockDefs: map[string]*blockSyntax{},
}
return s
c.Grammar = c
return c
}
// AddFilter adds a filter to settings.
func (s Config) AddFilter(name string, fn interface{}) {
s.Config.AddFilter(name, fn)
}
// func (s Config) AddFilter(name string, fn interface{}) {
// s.Config.AddFilter(name, fn)
// }

View File

@ -55,7 +55,7 @@ func (c renderContext) EvaluateString(source string) (out interface{}, err error
}
}
}()
return expression.EvaluateString(source, expression.NewContext(c.ctx.bindings, c.ctx.config.Config))
return expression.EvaluateString(source, expression.NewContext(c.ctx.bindings, c.ctx.config.Config.Config))
}
// Get gets a variable value within an evaluation context.

View File

@ -11,14 +11,15 @@ type nodeContext struct {
}
// newNodeContext creates a new evaluation context.
func newNodeContext(scope map[string]interface{}, s Config) nodeContext {
func newNodeContext(scope map[string]interface{}, c Config) nodeContext {
// The assign tag modifies the scope, so make a copy first.
// TODO this isn't really the right place for this.
vars := map[string]interface{}{}
for k, v := range scope {
vars[k] = v
}
return nodeContext{vars, s}
// fmt.Println("new", c.Config)
return nodeContext{vars, c}
}
// Clone makes a copy of a context, with copied bindings.
@ -43,5 +44,5 @@ func (c nodeContext) Evaluate(expr expression.Expression) (out interface{}, err
}
}
}()
return expr.Evaluate(expression.NewContext(c.bindings, c.config.Config))
return expr.Evaluate(expression.NewContext(c.bindings, c.config.Config.Config))
}

View File

@ -4,6 +4,7 @@ import (
"io"
"github.com/osteele/liquid/expression"
"github.com/osteele/liquid/parser"
)
// Node is a node of the render tree.
@ -12,7 +13,7 @@ type Node interface {
// BlockNode represents a {% tag %}…{% endtag %}.
type BlockNode struct {
Chunk
parser.Chunk
renderer func(io.Writer, Context) error
Body []Node
Branches []*BlockNode
@ -25,18 +26,18 @@ type RawNode struct {
// TagNode renders itself via a render function that is created during parsing.
type TagNode struct {
Chunk
parser.Chunk
renderer func(io.Writer, Context) error
}
// TextNode is a text chunk, that is rendered verbatim.
type TextNode struct {
Chunk
parser.Chunk
}
// ObjectNode is an {{ object }} object.
type ObjectNode struct {
Chunk
parser.Chunk
expr expression.Expression
}

View File

@ -20,8 +20,9 @@ func Errorf(format string, a ...interface{}) Error {
}
// Render renders the render tree.
func Render(node Node, w io.Writer, b map[string]interface{}, c Config) error {
return renderNode(node, w, newNodeContext(b, c))
func Render(node Node, w io.Writer, vars map[string]interface{}, c Config) error {
// fmt.Println("render", c)
return renderNode(node, w, newNodeContext(vars, c))
}
func renderNode(node Node, w io.Writer, ctx nodeContext) error { // nolint: gocyclo
@ -31,11 +32,11 @@ func renderNode(node Node, w io.Writer, ctx nodeContext) error { // nolint: gocy
case *BlockNode:
cd, ok := ctx.config.findBlockDef(n.Name)
if !ok || cd.parser == nil {
return parseErrorf("unknown tag: %s", n.Name)
return Errorf("unknown tag: %s", n.Name)
}
renderer := n.renderer
if renderer == nil {
panic(parseErrorf("unset renderer for %v", n))
panic(Errorf("unset renderer for %v", n))
}
return renderer(w, renderContext{ctx, nil, n})
case *RawNode:
@ -48,7 +49,7 @@ func renderNode(node Node, w io.Writer, ctx nodeContext) error { // nolint: gocy
case *ObjectNode:
value, err := ctx.Evaluate(n.expr)
if err != nil {
return parseErrorf("%s in %s", err, n.Source)
return Errorf("%s in %s", err, n.Source)
}
return writeObject(value, w)
case *SeqNode:
@ -61,7 +62,7 @@ func renderNode(node Node, w io.Writer, ctx nodeContext) error { // nolint: gocy
_, err := w.Write([]byte(n.Source))
return err
default:
panic(parseErrorf("unknown node type %T", node))
panic(Errorf("unknown node type %T", node))
}
return nil
}

View File

@ -7,14 +7,14 @@ import (
)
type template struct {
ast render.ASTNode
config render.Config
root render.Node
config *render.Config
}
// Render executes the template within the bindings environment.
func (t *template) Render(b Bindings) ([]byte, error) {
func (t *template) Render(vars Bindings) ([]byte, error) {
buf := new(bytes.Buffer)
err := render.Render(t.ast, buf, b, t.config)
err := render.Render(t.root, buf, vars, *t.config)
if err != nil {
return nil, err
}