mirror of
https://github.com/danog/liquid.git
synced 2024-11-26 23:24:38 +01:00
Implement whitespace control
This commit is contained in:
parent
bf43fb85d1
commit
f9ac12bb26
@ -1,14 +1,14 @@
|
||||
# Go Liquid Template Parser
|
||||
# Liquid Template Parser
|
||||
|
||||
[![][travis-svg]][travis-url] [![][coveralls-svg]][coveralls-url] [![][go-report-card-svg]][go-report-card-url] [![][godoc-svg]][godoc-url] [![][license-svg]][license-url]
|
||||
|
||||
`liquid` ports [Shopify Liquid templates](https://shopify.github.io/liquid) to Go. It was developed for use in the [Gojekyll](https://github.com/osteele/gojekyll) static site generator.
|
||||
`liquid` is a Go implementation of [Shopify Liquid templates](https://shopify.github.io/liquid). It was developed for use in the [Gojekyll](https://github.com/osteele/gojekyll) static site generator.
|
||||
|
||||
> “Any sufficiently complicated C or Fortran program contains an ad-hoc, informally-specified, bug-ridden, slow implementation of half of Common Lisp.” – Philip Greenspun
|
||||
|
||||
<!-- TOC -->
|
||||
|
||||
- [Go Liquid Template Parser](#go-liquid-template-parser)
|
||||
- [Liquid Template Parser](#liquid-template-parser)
|
||||
- [Differences from Liquid](#differences-from-liquid)
|
||||
- [Stability](#stability)
|
||||
- [Install](#install)
|
||||
@ -31,7 +31,6 @@ The [feature parity board](https://github.com/osteele/liquid/projects/1) lists d
|
||||
In brief, these aren't implemented:
|
||||
|
||||
- Warn and lax [error modes](https://github.com/shopify/liquid#error-modes).
|
||||
- Whitespace control
|
||||
- Non-strict filters. An undefined filter is currently an error.
|
||||
- Strict variables. An undefined variable is not an error.
|
||||
|
||||
|
@ -101,7 +101,7 @@ func (c rendererContext) ExpandTagArg() (string, error) {
|
||||
return "", err
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
err = root.render(buf, c.ctx)
|
||||
err = Render(root, buf, c.ctx.bindings, c.ctx.config)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -137,7 +137,7 @@ func (c rendererContext) RenderFile(filename string, b map[string]interface{}) (
|
||||
c.ctx.bindings[k] = v
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
if err := root.render(buf, nc); err != nil {
|
||||
if err := Render(root, buf, nc.bindings, nc.config); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), nil
|
||||
|
@ -59,13 +59,12 @@ var contextTestBindings = map[string]interface{}{
|
||||
func TestContext(t *testing.T) {
|
||||
cfg := NewConfig()
|
||||
addContextTestTags(cfg)
|
||||
context := newNodeContext(contextTestBindings, cfg)
|
||||
for i, test := range contextTests {
|
||||
t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) {
|
||||
root, err := cfg.Compile(test.in, parser.SourceLoc{})
|
||||
require.NoErrorf(t, err, test.in)
|
||||
buf := new(bytes.Buffer)
|
||||
err = root.render(buf, context)
|
||||
err = Render(root, buf, contextTestBindings, cfg)
|
||||
require.NoErrorf(t, err, test.in)
|
||||
require.Equalf(t, test.out, buf.String(), test.in)
|
||||
})
|
||||
|
@ -11,7 +11,7 @@ import (
|
||||
type Node interface {
|
||||
SourceLocation() parser.SourceLoc // for error reporting
|
||||
SourceText() string // for error reporting
|
||||
render( io.Writer, nodeContext) Error
|
||||
render(*trimWriter, nodeContext) Error
|
||||
}
|
||||
|
||||
// BlockNode represents a {% tag %}…{% endtag %}.
|
||||
|
@ -11,10 +11,24 @@ import (
|
||||
|
||||
// Render renders the render tree.
|
||||
func Render(node Node, w io.Writer, vars map[string]interface{}, c Config) Error {
|
||||
return node.render(w, newNodeContext(vars, c))
|
||||
tw := trimWriter{w: w}
|
||||
defer tw.Flush()
|
||||
return node.render(&tw, newNodeContext(vars, c))
|
||||
}
|
||||
|
||||
func (n *BlockNode) render(w io.Writer, ctx nodeContext) Error {
|
||||
// RenderASTSequence renders a sequence of nodes.
|
||||
func (c nodeContext) RenderSequence(w io.Writer, seq []Node) Error {
|
||||
tw := trimWriter{w: w}
|
||||
defer tw.Flush()
|
||||
for _, n := range seq {
|
||||
if err := n.render(&tw, c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *BlockNode) render(w *trimWriter, ctx nodeContext) Error {
|
||||
cd, ok := ctx.config.findBlockDef(n.Name)
|
||||
if !ok || cd.parser == nil {
|
||||
// this should have been detected during compilation; it's an implementation error if it happens here
|
||||
@ -28,7 +42,7 @@ func (n *BlockNode) render(w io.Writer, ctx nodeContext) Error {
|
||||
return wrapRenderError(err, n)
|
||||
}
|
||||
|
||||
func (n *RawNode) render(w io.Writer, ctx nodeContext) Error {
|
||||
func (n *RawNode) render(w *trimWriter, ctx nodeContext) Error {
|
||||
for _, s := range n.slices {
|
||||
_, err := io.WriteString(w, s)
|
||||
if err != nil {
|
||||
@ -38,15 +52,20 @@ func (n *RawNode) render(w io.Writer, ctx nodeContext) Error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *ObjectNode) render(w io.Writer, ctx nodeContext) Error {
|
||||
func (n *ObjectNode) render(w *trimWriter, ctx nodeContext) Error {
|
||||
w.TrimLeft(n.TrimLeft)
|
||||
value, err := ctx.Evaluate(n.expr)
|
||||
if err != nil {
|
||||
return wrapRenderError(err, n)
|
||||
}
|
||||
return wrapRenderError(writeObject(value, w), n)
|
||||
if err := wrapRenderError(writeObject(value, w), n); err != nil {
|
||||
return err
|
||||
}
|
||||
w.TrimRight(n.TrimRight)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *SeqNode) render(w io.Writer, ctx nodeContext) Error {
|
||||
func (n *SeqNode) render(w *trimWriter, ctx nodeContext) Error {
|
||||
for _, c := range n.Children {
|
||||
if err := c.render(w, ctx); err != nil {
|
||||
return err
|
||||
@ -55,11 +74,14 @@ func (n *SeqNode) render(w io.Writer, ctx nodeContext) Error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *TagNode) render(w io.Writer, ctx nodeContext) Error {
|
||||
return wrapRenderError(n.renderer(w, rendererContext{ctx, n, nil}), n)
|
||||
func (n *TagNode) render(w *trimWriter, ctx nodeContext) Error {
|
||||
w.TrimLeft(n.TrimLeft)
|
||||
err := wrapRenderError(n.renderer(w, rendererContext{ctx, n, nil}), n)
|
||||
w.TrimRight(n.TrimRight)
|
||||
return err
|
||||
}
|
||||
|
||||
func (n *TextNode) render(w io.Writer, ctx nodeContext) Error {
|
||||
func (n *TextNode) render(w *trimWriter, ctx nodeContext) Error {
|
||||
_, err := io.WriteString(w, n.Source)
|
||||
return wrapRenderError(err, n)
|
||||
}
|
||||
@ -87,13 +109,3 @@ func writeObject(value interface{}, w io.Writer) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// RenderASTSequence renders a sequence of nodes.
|
||||
func (c nodeContext) RenderSequence(w io.Writer, seq []Node) Error {
|
||||
for _, n := range seq {
|
||||
if err := n.render(w, c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -12,6 +12,15 @@ import (
|
||||
)
|
||||
|
||||
func addRenderTestTags(s Config) {
|
||||
s.AddTag("y", func(string) (func(io.Writer, Context) error, error) {
|
||||
return func(w io.Writer, _ Context) error {
|
||||
_, err := io.WriteString(w, "y")
|
||||
return err
|
||||
}, nil
|
||||
})
|
||||
s.AddTag("null", func(string) (func(io.Writer, Context) error, error) {
|
||||
return func(io.Writer, Context) error { return nil }, nil
|
||||
})
|
||||
s.AddBlock("errblock").Compiler(func(c BlockNode) (func(io.Writer, Context) error, error) {
|
||||
return func(w io.Writer, c Context) error {
|
||||
return fmt.Errorf("errblock error")
|
||||
@ -20,16 +29,42 @@ func addRenderTestTags(s Config) {
|
||||
}
|
||||
|
||||
var renderTests = []struct{ in, out string }{
|
||||
// literals representations
|
||||
{`{{ nil }}`, ""},
|
||||
{`{{ true }}`, "true"},
|
||||
{`{{ false }}`, "false"},
|
||||
{`{{ 12 }}`, "12"},
|
||||
{`{{ 12.3 }}`, "12.3"},
|
||||
{`{{ "abc" }}`, "abc"},
|
||||
{`{{ array }}`, "firstsecondthird"},
|
||||
|
||||
// variables and properties
|
||||
{`{{ x }}`, "123"},
|
||||
{`{{ page.title }}`, "Introduction"},
|
||||
{`{{ array }}`, "firstsecondthird"},
|
||||
{`{{ array[1] }}`, "second"},
|
||||
|
||||
// whitespace control
|
||||
// {` {{ 1 }} `, " 1 "},
|
||||
{` {{- 1 }} `, "1 "},
|
||||
{` {{ 1 -}} `, " 1"},
|
||||
{` {{- 1 -}} `, "1"},
|
||||
{` {{- nil -}} `, ""},
|
||||
{`x {{ 1 }} z`, "x 1 z"},
|
||||
{`x {{- 1 }} z`, "x1 z"},
|
||||
{`x {{ 1 -}} z`, "x 1z"},
|
||||
{`x {{- 1 -}} z`, "x1z"},
|
||||
{`x {{ nil }} z`, "x z"},
|
||||
{`x {{- nil }} z`, "x z"},
|
||||
{`x {{ nil -}} z`, "x z"},
|
||||
{`x {{- nil -}} z`, "xz"},
|
||||
{`x {% null %} z`, "x z"},
|
||||
{`x {%- null %} z`, "x z"},
|
||||
{`x {% null -%} z`, "x z"},
|
||||
{`x {%- null -%} z`, "xz"},
|
||||
{`x {% y %} z`, "x y z"},
|
||||
{`x {%- y %} z`, "xy z"},
|
||||
{`x {% y -%} z`, "x yz"},
|
||||
{`x {%- y -%} z`, "xyz"},
|
||||
}
|
||||
|
||||
var renderErrorTests = []struct{ in, out string }{
|
||||
@ -66,13 +101,12 @@ var renderTestBindings = map[string]interface{}{
|
||||
func TestRender(t *testing.T) {
|
||||
cfg := NewConfig()
|
||||
addRenderTestTags(cfg)
|
||||
context := newNodeContext(renderTestBindings, cfg)
|
||||
for i, test := range renderTests {
|
||||
t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) {
|
||||
root, err := cfg.Compile(test.in, parser.SourceLoc{})
|
||||
require.NoErrorf(t, err, test.in)
|
||||
buf := new(bytes.Buffer)
|
||||
err = root.render(buf, context)
|
||||
err = Render(root, buf, renderTestBindings, cfg)
|
||||
require.NoErrorf(t, err, test.in)
|
||||
require.Equalf(t, test.out, buf.String(), test.in)
|
||||
})
|
||||
@ -82,12 +116,11 @@ func TestRender(t *testing.T) {
|
||||
func TestRenderErrors(t *testing.T) {
|
||||
cfg := NewConfig()
|
||||
addRenderTestTags(cfg)
|
||||
context := newNodeContext(renderTestBindings, cfg)
|
||||
for i, test := range renderErrorTests {
|
||||
t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) {
|
||||
root, err := cfg.Compile(test.in, parser.SourceLoc{})
|
||||
require.NoErrorf(t, err, test.in)
|
||||
err = root.render(ioutil.Discard, context)
|
||||
err = Render(root, ioutil.Discard, renderTestBindings, cfg)
|
||||
require.Errorf(t, err, test.in)
|
||||
require.Containsf(t, err.Error(), test.out, test.in)
|
||||
})
|
||||
|
51
render/trimwriter.go
Normal file
51
render/trimwriter.go
Normal file
@ -0,0 +1,51 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type trimWriter struct {
|
||||
w io.Writer
|
||||
buf []byte
|
||||
trimRight bool
|
||||
}
|
||||
|
||||
func (tw *trimWriter) Write(b []byte) (int, error) {
|
||||
if tw.trimRight {
|
||||
b = bytes.TrimLeftFunc(b, unicode.IsSpace)
|
||||
} else if len(tw.buf) > 0 {
|
||||
_, err := tw.w.Write(tw.buf)
|
||||
tw.buf = []byte{}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
nonWS := bytes.TrimRightFunc(b, unicode.IsSpace)
|
||||
if len(nonWS) < len(b) {
|
||||
tw.buf = append(tw.buf, b[len(nonWS):]...)
|
||||
}
|
||||
return tw.w.Write(nonWS)
|
||||
}
|
||||
func (tw *trimWriter) Flush() (err error) {
|
||||
if tw.buf != nil {
|
||||
_, err = tw.w.Write(tw.buf)
|
||||
tw.buf = []byte{}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (tw *trimWriter) TrimLeft(f bool) {
|
||||
if !f {
|
||||
if err := tw.Flush(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
tw.buf = []byte{}
|
||||
tw.trimRight = false
|
||||
}
|
||||
|
||||
func (tw *trimWriter) TrimRight(f bool) {
|
||||
tw.trimRight = f
|
||||
}
|
Loading…
Reference in New Issue
Block a user