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:
parent
18e2540b3a
commit
903acb8d2e
20
README.md
20
README.md
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
})
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
|
@ -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.
|
@ -1,4 +1,4 @@
|
||||
package render
|
||||
package parser
|
||||
|
||||
import "fmt"
|
||||
|
@ -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
15
parser/config.go
Normal 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()}
|
||||
}
|
@ -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 }
|
@ -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})
|
@ -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)
|
||||
})
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package render
|
||||
package parser
|
||||
|
||||
import (
|
||||
"regexp"
|
@ -1,4 +1,4 @@
|
||||
package render
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
// }
|
||||
|
@ -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.
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user