Skip to content

Commit

Permalink
Merge pull request #88 from jamslinger/trim-whitespace
Browse files Browse the repository at this point in the history
Fix whitespace control
  • Loading branch information
danog authored Oct 17, 2024
2 parents 69a6f0e + 4c006c1 commit 0f5b61d
Show file tree
Hide file tree
Showing 10 changed files with 240 additions and 84 deletions.
14 changes: 14 additions & 0 deletions parser/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,20 @@ type ASTSeq struct {
sourcelessNode
}

// TrimDirection determines the trim direction of an ASTTrim object.
type TrimDirection int

const (
Left TrimDirection = iota
Right
)

// ASTTrim is a trim object.
type ASTTrim struct {
sourcelessNode
TrimDirection
}

// It shouldn't be possible to get an error from one of these node types.
// If it is, this needs to be re-thought to figure out where the source
// location comes from.
Expand Down
4 changes: 4 additions & 0 deletions parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ func (c Config) parseTokens(tokens []Token) (ASTNode, Error) { // nolint: gocycl
} else {
*ap = append(*ap, &ASTTag{tok})
}
case tok.Type == TrimLeftTokenType:
*ap = append(*ap, &ASTTrim{TrimDirection: Left})
case tok.Type == TrimRightTokenType:
*ap = append(*ap, &ASTTrim{TrimDirection: Right})
}
}
if bn != nil {
Expand Down
27 changes: 21 additions & 6 deletions parser/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,28 +27,43 @@ func Scan(data string, loc SourceLoc, delims []string) (tokens []Token) {
source := data[ts:te]
switch {
case data[ts:ts+len(delims[0])] == delims[0]:
tok := Token{
if source[2] == '-' {
tokens = append(tokens, Token{
Type: TrimLeftTokenType,
})
}
tokens = append(tokens, Token{
Type: ObjTokenType,
SourceLoc: loc,
Source: source,
Args: data[m[2]:m[3]],
TrimLeft: source[2] == '-',
TrimRight: source[len(source)-3] == '-',
})
if source[len(source)-3] == '-' {
tokens = append(tokens, Token{
Type: TrimRightTokenType,
})
}
tokens = append(tokens, tok)
case data[ts:ts+len(delims[2])] == delims[2]:
if source[2] == '-' {
tokens = append(tokens, Token{
Type: TrimLeftTokenType,
})
}
tok := Token{
Type: TagTokenType,
SourceLoc: loc,
Source: source,
Name: data[m[4]:m[5]],
TrimLeft: source[2] == '-',
TrimRight: source[len(source)-3] == '-',
}
if m[6] > 0 {
tok.Args = data[m[6]:m[7]]
}
tokens = append(tokens, tok)
if source[len(source)-3] == '-' {
tokens = append(tokens, Token{
Type: TrimRightTokenType,
})
}
}
loc.LineNo += strings.Count(source, "\n")
p = te
Expand Down
91 changes: 73 additions & 18 deletions parser/scanner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,29 +72,84 @@ func TestScan_ws(t *testing.T) {
scan := func(src string) []Token { return Scan(src, SourceLoc{}, nil) }

wsTests := []struct {
in, expect string
left, right bool
in string
exp []Token
}{
{`{{ expr }}`, "expr", false, false},
{`{{- expr }}`, "expr", true, false},
{`{{ expr -}}`, "expr", false, true},
{`{% tag arg %}`, "tag", false, false},
{`{%- tag arg %}`, "tag", true, false},
{`{% tag arg -%}`, "tag", false, true},
{`{{ expr }}`, []Token{
{
Type: ObjTokenType,
Args: "expr",
Source: "{{ expr }}",
},
}},
{`{{- expr }}`, []Token{
{
Type: TrimLeftTokenType,
},
{
Type: ObjTokenType,
Args: "expr",
Source: "{{- expr }}",
},
}},
{`{{ expr -}}`, []Token{
{
Type: ObjTokenType,
Args: "expr",
Source: "{{ expr -}}",
},
{
Type: TrimRightTokenType,
},
}},
{`{{- expr -}}`, []Token{
{
Type: TrimLeftTokenType,
},
{
Type: ObjTokenType,
Args: "expr",
Source: "{{- expr -}}",
},
{
Type: TrimRightTokenType,
},
}},
{`{% tag arg %}`, []Token{
{
Type: TagTokenType,
Name: "tag",
Args: "arg",
Source: "{% tag arg %}",
},
}},
{`{%- tag arg %}`, []Token{
{
Type: TrimLeftTokenType,
},
{
Type: TagTokenType,
Name: "tag",
Args: "arg",
Source: "{%- tag arg %}",
},
}},
{`{% tag arg -%}`, []Token{
{
Type: TagTokenType,
Name: "tag",
Args: "arg",
Source: "{% tag arg -%}",
},
{
Type: TrimRightTokenType,
},
}},
}
for i, test := range wsTests {
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
tokens := scan(test.in)
require.Len(t, tokens, 1)
tok := tokens[0]
if test.expect == "tag" {
require.Equalf(t, "tag", tok.Name, test.in)
require.Equalf(t, "arg", tok.Args, test.in)
} else {
require.Equalf(t, "expr", tok.Args, test.in)
}
require.Equalf(t, test.left, tok.TrimLeft, test.in)
require.Equalf(t, test.right, tok.TrimRight, test.in)
require.Equalf(t, test.exp, tokens, test.in)
})
}
}
Expand Down
17 changes: 11 additions & 6 deletions parser/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@ import "fmt"

// A Token is an object {{ a.b }}, a tag {% if a>b %}, or a text chunk (anything outside of {{}} and {%%}.)
type Token struct {
Type TokenType
SourceLoc SourceLoc
Name string // Name is the tag name of a tag Chunk. E.g. the tag name of "{% if 1 %}" is "if".
Args string // Parameters is the tag arguments of a tag Chunk. E.g. the tag arguments of "{% if 1 %}" is "1".
Source string // Source is the entirety of the token, including the "{{", "{%", etc. markers.
TrimLeft, TrimRight bool // Trim whitespace left or right of this token; from {{- tag -}} and {%- expr -%}
Type TokenType
SourceLoc SourceLoc
Name string // Name is the tag name of a tag Chunk. E.g. the tag name of "{% if 1 %}" is "if".
Args string // Parameters is the tag arguments of a tag Chunk. E.g. the tag arguments of "{% if 1 %}" is "1".
Source string // Source is the entirety of the token, including the "{{", "{%", etc. markers.
}

// TokenType is the type of a Chunk
Expand All @@ -24,6 +23,10 @@ const (
TagTokenType
// ObjTokenType is the type of an object Chunk "{{…}}"
ObjTokenType
// TrimLeftTokenType is the type of a left trim tag "-"
TrimLeftTokenType
// TrimRightTokenType is the type of a right trim tag "-"
TrimRightTokenType
)

// SourceLoc contains a Token's source location. Pathname is in the local file
Expand Down Expand Up @@ -53,6 +56,8 @@ func (c Token) String() string {
return fmt.Sprintf("%v{Tag:%#v, Args:%#v}", c.Type, c.Name, c.Args)
case ObjTokenType:
return fmt.Sprintf("%v{%#v}", c.Type, c.Args)
case TrimLeftTokenType, TrimRightTokenType:
return "-"
default:
return fmt.Sprintf("%v{%#v}", c.Type, c.Source)
}
Expand Down
2 changes: 2 additions & 0 deletions render/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ func (c Config) compileNode(n parser.ASTNode) (Node, parser.Error) {
return &TextNode{n.Token}, nil
case *parser.ASTObject:
return &ObjectNode{n.Token, n.Expr}, nil
case *parser.ASTTrim:
return &TrimNode{TrimDirection: n.TrimDirection}, nil
default:
panic(fmt.Errorf("un-compilable node type %T", n))
}
Expand Down
6 changes: 6 additions & 0 deletions render/nodes.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ type SeqNode struct {
sourcelessNode
}

// TrimNode is a trim object.
type TrimNode struct {
sourcelessNode
parser.TrimDirection
}

// FIXME requiring this is a bad design
type sourcelessNode struct{}

Expand Down
31 changes: 19 additions & 12 deletions render/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package render
import (
"errors"
"fmt"
"github.com/osteele/liquid/parser"
"io"
"reflect"
"time"
Expand All @@ -17,21 +18,24 @@ func Render(node Node, w io.Writer, vars map[string]interface{}, c Config) Error
if err := node.render(&tw, newNodeContext(vars, c)); err != nil {
return err
}
if err := tw.Flush(); err != nil {
if _, err := tw.Flush(); err != nil {
panic(err)
}
return nil
}

// RenderASTSequence renders a sequence of nodes.
// RenderSequence renders a sequence of nodes.
func (c nodeContext) RenderSequence(w io.Writer, seq []Node) Error {
tw := trimWriter{w: w}
tw, ok := w.(*trimWriter)
if !ok {
tw = &trimWriter{w: w}
}
for _, n := range seq {
if err := n.render(&tw, c); err != nil {
if err := n.render(tw, c); err != nil {
return err
}
}
if err := tw.Flush(); err != nil {
if _, err := tw.Flush(); err != nil {
panic(err)
}
return nil
Expand All @@ -47,9 +51,7 @@ func (n *BlockNode) render(w *trimWriter, ctx nodeContext) Error {
if renderer == nil {
panic(fmt.Errorf("unset renderer for %v", n))
}
w.TrimLeft(n.TrimLeft)
err := renderer(w, rendererContext{ctx, nil, n})
w.TrimRight(n.TrimRight)
return wrapRenderError(err, n)
}

Expand All @@ -64,7 +66,6 @@ func (n *RawNode) render(w *trimWriter, 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)
Expand All @@ -75,7 +76,6 @@ func (n *ObjectNode) render(w *trimWriter, ctx nodeContext) Error {
if err := wrapRenderError(writeObject(w, value), n); err != nil {
return err
}
w.TrimRight(n.TrimRight)
return nil
}

Expand All @@ -89,17 +89,24 @@ func (n *SeqNode) render(w *trimWriter, ctx nodeContext) Error {
}

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 *trimWriter, ctx nodeContext) Error {
func (n *TextNode) render(w *trimWriter, _ nodeContext) Error {
_, err := io.WriteString(w, n.Source)
return wrapRenderError(err, n)
}

func (n *TrimNode) render(w *trimWriter, _ nodeContext) Error {
if n.TrimDirection == parser.Left {
return wrapRenderError(w.TrimLeft(), n)
} else {
w.TrimRight()
return nil
}
}

// writeObject writes a value used in an object node
func writeObject(w io.Writer, value interface{}) error {
value = values.ToLiquid(value)
Expand Down
Loading

0 comments on commit 0f5b61d

Please sign in to comment.