1
0
mirror of https://github.com/danog/liquid.git synced 2024-11-26 21:24:40 +01:00

Implement whitespace control

This commit is contained in:
Oliver Steele 2017-07-16 17:43:04 -04:00
parent bf43fb85d1
commit f9ac12bb26
7 changed files with 127 additions and 33 deletions

View File

@ -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.

View File

@ -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

View File

@ -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)
})

View File

@ -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 %}.

View File

@ -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
}

View File

@ -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
View 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
}