Skip to content

Commit

Permalink
Render tree is distinct type from parse AST
Browse files Browse the repository at this point in the history
  • Loading branch information
osteele committed Jul 6, 2017
1 parent 8f63cb7 commit 803471c
Show file tree
Hide file tree
Showing 10 changed files with 123 additions and 80 deletions.
2 changes: 0 additions & 2 deletions render/ast.go
Expand Up @@ -8,13 +8,11 @@ import (

// ASTNode is a node of an AST.
type ASTNode interface {
// Render evaluates an AST node and writes the result to an io.Writer.
}

// ASTBlock represents a {% tag %}…{% endtag %}.
type ASTBlock struct {
Chunk
renderer func(io.Writer, Context) error
syntax BlockSyntax
Body []ASTNode
Branches []*ASTBlock
Expand Down
34 changes: 17 additions & 17 deletions render/blocks.go
Expand Up @@ -6,9 +6,9 @@ import (
)

// BlockParser builds a renderer for the tag instance.
type BlockParser func(ASTBlock) (func(io.Writer, Context) error, error)
type BlockParser func(BlockNode) (func(io.Writer, Context) error, error)

// blockDef tells the parser how to parse control tags.
// blockDef tells the parser how to parse control tagc.
type blockDef struct {
name string
isBranchTag, isEndTag bool
Expand Down Expand Up @@ -42,38 +42,38 @@ func (c *blockDef) ParentTags() []string {
}
func (c *blockDef) TagName() string { return c.name }

func (s Config) addBlockDef(ct *blockDef) {
s.blockDefs[ct.name] = ct
func (c Config) addBlockDef(ct *blockDef) {
c.blockDefs[ct.name] = ct
}

func (s Config) findBlockDef(name string) (*blockDef, bool) {
ct, found := s.blockDefs[name]
func (c Config) findBlockDef(name string) (*blockDef, bool) {
ct, found := c.blockDefs[name]
return ct, found
}

// BlockSyntax is part of the Grammar interface.
func (s Config) BlockSyntax(name string) (BlockSyntax, bool) {
ct, found := s.blockDefs[name]
func (c Config) BlockSyntax(name string) (BlockSyntax, bool) {
ct, found := c.blockDefs[name]
return ct, found
}

type blockDefBuilder struct {
s Config
cfg Config
tag *blockDef
}

// AddBlock defines a control tag and its matching end tag.
func (s Config) AddBlock(name string) blockDefBuilder { // nolint: golint
func (c Config) AddBlock(name string) blockDefBuilder { // nolint: golint
ct := &blockDef{name: name}
s.addBlockDef(ct)
s.addBlockDef(&blockDef{name: "end" + name, isEndTag: true, parent: ct})
return blockDefBuilder{s, ct}
c.addBlockDef(ct)
c.addBlockDef(&blockDef{name: "end" + name, isEndTag: true, parent: ct})
return blockDefBuilder{c, ct}
}

// Branch tells the parser that the named tag can appear immediately between this tag and its end tag,
// so long as it is not nested within any other control tags.
// so long as it is not nested within any other control tagc.
func (b blockDefBuilder) Branch(name string) blockDefBuilder {
b.s.addBlockDef(&blockDef{name: name, isBranchTag: true, parent: b.tag})
b.cfg.addBlockDef(&blockDef{name: name, isBranchTag: true, parent: b.tag})
return b
}

Expand All @@ -84,7 +84,7 @@ func (b blockDefBuilder) Governs(_ []string) blockDefBuilder {

// SameSyntaxAs tells the parser that this tag has the same syntax as the named tag.
func (b blockDefBuilder) SameSyntaxAs(name string) blockDefBuilder {
rt := b.s.blockDefs[name]
rt := b.cfg.blockDefs[name]
if rt == nil {
panic(fmt.Errorf("undefined: %s", name))
}
Expand All @@ -99,7 +99,7 @@ func (b blockDefBuilder) Parser(fn BlockParser) {

// Renderer sets the render action for a control tag definition.
func (b blockDefBuilder) Renderer(fn func(io.Writer, Context) error) {
b.tag.parser = func(node ASTBlock) (func(io.Writer, Context) error, error) {
b.tag.parser = func(node BlockNode) (func(io.Writer, Context) error, error) {
// TODO parse error if there are arguments?
return fn, nil
}
Expand Down
42 changes: 20 additions & 22 deletions render/compiler.go
Expand Up @@ -2,7 +2,6 @@ package render

import (
"fmt"
"io"
)

// A CompilationError is a parse error during template compilation.
Expand All @@ -24,7 +23,7 @@ func (c Config) Compile(source string) (ASTNode, error) {
}

// nolint: gocyclo
func (c Config) compileNode(n ASTNode) (ASTNode, error) {
func (c Config) compileNode(n ASTNode) (Node, error) {
switch n := n.(type) {
case *ASTBlock:
body, err := c.compileNodes(n.Body)
Expand All @@ -40,54 +39,53 @@ func (c Config) compileNode(n ASTNode) (ASTNode, error) {
if !ok {
return nil, compilationErrorf("undefined tag %q", n.Name)
}
var renderer func(io.Writer, Context) error
node := BlockNode{
Chunk: n.Chunk,
syntax: n.syntax,
Body: body,
Branches: branches,
}
if cd.parser != nil {
r, err := cd.parser(*n)
r, err := cd.parser(node)
if err != nil {
return nil, err
}
renderer = r
node.renderer = r
}
return &ASTBlock{
Chunk: n.Chunk,
renderer: renderer,
syntax: n.syntax,
Body: body,
Branches: branches,
}, nil
return &node, nil
case *ASTFunctional:
return &ASTFunctional{n.Chunk, n.render}, nil
return &FunctionalNode{n.Chunk, n.render}, nil
case *ASTRaw:
return &ASTRaw{n.slices}, nil
return &RawNode{n.slices}, nil
case *ASTSeq:
children, err := c.compileNodes(n.Children)
if err != nil {
return nil, err
}
return &ASTSeq{children}, nil
return &SeqNode{children}, nil
case *ASTText:
return &ASTText{n.Chunk}, nil
return &TextNode{n.Chunk}, nil
case *ASTObject:
return &ASTObject{n.Chunk, n.expr}, nil
return &ObjectNode{n.Chunk, n.expr}, nil
default:
panic(fmt.Errorf("un-compilable node type %T", n))
}
}

func (c Config) compileBlocks(blocks []*ASTBlock) ([]*ASTBlock, error) {
out := make([]*ASTBlock, 0, len(blocks))
func (c Config) compileBlocks(blocks []*ASTBlock) ([]*BlockNode, error) {
out := make([]*BlockNode, 0, len(blocks))
for _, child := range blocks {
compiled, err := c.compileNode(child)
if err != nil {
return nil, err
}
out = append(out, compiled.(*ASTBlock))
out = append(out, compiled.(*BlockNode))
}
return out, nil
}

func (c Config) compileNodes(nodes []ASTNode) ([]ASTNode, error) {
out := make([]ASTNode, 0, len(nodes))
func (c Config) compileNodes(nodes []ASTNode) ([]Node, error) {
out := make([]Node, 0, len(nodes))
for _, child := range nodes {
compiled, err := c.compileNode(child)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion render/compiler_test.go
Expand Up @@ -9,7 +9,7 @@ import (
)

func addCompilerTestTags(s Config) {
s.AddBlock("block").Parser(func(c ASTBlock) (func(io.Writer, Context) error, error) {
s.AddBlock("block").Parser(func(c BlockNode) (func(io.Writer, Context) error, error) {
return nil, fmt.Errorf("block compiler error")
})
}
Expand Down
12 changes: 6 additions & 6 deletions render/context.go
Expand Up @@ -18,7 +18,7 @@ type Context interface {
EvaluateStatement(tag, source string) (interface{}, error)
ExpandTagArg() (string, error)
InnerString() (string, error)
RenderChild(io.Writer, *ASTBlock) error
RenderChild(io.Writer, *BlockNode) error
RenderChildren(io.Writer) error
RenderFile(string, map[string]interface{}) (string, error)
Set(name string, value interface{})
Expand All @@ -29,8 +29,8 @@ type Context interface {

type renderContext struct {
ctx nodeContext
node *ASTFunctional
cn *ASTBlock
node *FunctionalNode
cn *BlockNode
}

// Evaluate evaluates an expression within the template context.
Expand Down Expand Up @@ -81,16 +81,16 @@ func (c renderContext) ExpandTagArg() (string, error) {
}

// RenderChild renders a node.
func (c renderContext) RenderChild(w io.Writer, b *ASTBlock) error {
return c.ctx.RenderASTSequence(w, b.Body)
func (c renderContext) RenderChild(w io.Writer, b *BlockNode) error {
return c.ctx.RenderSequence(w, b.Body)
}

// RenderChildren renders the current node's children.
func (c renderContext) RenderChildren(w io.Writer) error {
if c.cn == nil {
return nil
}
return c.ctx.RenderASTSequence(w, c.cn.Body)
return c.ctx.RenderSequence(w, c.cn.Body)
}

func (c renderContext) RenderFile(filename string, b map[string]interface{}) (string, error) {
Expand Down
46 changes: 23 additions & 23 deletions render/render.go
Expand Up @@ -19,32 +19,16 @@ func Errorf(format string, a ...interface{}) Error {
return Error(fmt.Sprintf(format, a...))
}

// Render renders the AST rooted at node to the writer.
func Render(node ASTNode, w io.Writer, b map[string]interface{}, c Config) 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 renderNode(node ASTNode, w io.Writer, ctx nodeContext) error { // nolint: gocyclo
func renderNode(node Node, w io.Writer, ctx nodeContext) error { // nolint: gocyclo
switch n := node.(type) {
case *ASTSeq:
for _, c := range n.Children {
if err := renderNode(c, w, ctx); err != nil {
return err
}
}
case *ASTFunctional:
case *FunctionalNode:
return n.render(w, renderContext{ctx, n, nil})
case *ASTText:
_, err := w.Write([]byte(n.Source))
return err
case *ASTRaw:
for _, s := range n.slices {
_, err := w.Write([]byte(s))
if err != nil {
return err
}
}
case *ASTBlock:
case *BlockNode:
cd, ok := ctx.config.findBlockDef(n.Name)
if !ok || cd.parser == nil {
return parseErrorf("unknown tag: %s", n.Name)
Expand All @@ -54,12 +38,28 @@ func renderNode(node ASTNode, w io.Writer, ctx nodeContext) error { // nolint: g
panic(parseErrorf("unset renderer for %v", n))
}
return renderer(w, renderContext{ctx, nil, n})
case *ASTObject:
case *RawNode:
for _, s := range n.slices {
_, err := w.Write([]byte(s))
if err != nil {
return err
}
}
case *ObjectNode:
value, err := ctx.Evaluate(n.expr)
if err != nil {
return parseErrorf("%s in %s", err, n.Source)
}
return writeObject(value, w)
case *SeqNode:
for _, c := range n.Children {
if err := renderNode(c, w, ctx); err != nil {
return err
}
}
case *TextNode:
_, err := w.Write([]byte(n.Source))
return err
default:
panic(parseErrorf("unknown node type %T", node))
}
Expand Down Expand Up @@ -91,7 +91,7 @@ func writeObject(value interface{}, w io.Writer) error {
}

// RenderASTSequence renders a sequence of nodes.
func (c nodeContext) RenderASTSequence(w io.Writer, seq []ASTNode) error {
func (c nodeContext) RenderSequence(w io.Writer, seq []Node) error {
for _, n := range seq {
if err := renderNode(n, w, c); err != nil {
return err
Expand Down
47 changes: 47 additions & 0 deletions render/render_node.go
@@ -0,0 +1,47 @@
package render

import (
"io"

"github.com/osteele/liquid/expression"
)

// Node is a node of the render tree.
type Node interface {
}

// BlockNode represents a {% tag %}…{% endtag %}.
type BlockNode struct {
Chunk
renderer func(io.Writer, Context) error
syntax BlockSyntax
Body []Node
Branches []*BlockNode
}

// RawNode holds the text between the start and end of a raw tag.
type RawNode struct {
slices []string
}

// FunctionalNode renders itself via a render function that is created during parsing.
type FunctionalNode struct {
Chunk
render func(io.Writer, Context) error
}

// TextNode is a text chunk, that is rendered verbatim.
type TextNode struct {
Chunk
}

// ObjectNode is an {{ object }} object.
type ObjectNode struct {
Chunk
expr expression.Expression
}

// SeqNode is a sequence of nodes.
type SeqNode struct {
Children []Node
}
4 changes: 2 additions & 2 deletions render/render_test.go
Expand Up @@ -11,7 +11,7 @@ import (
)

func addRenderTestTags(s Config) {
s.AddBlock("parse").Parser(func(c ASTBlock) (func(io.Writer, Context) error, error) {
s.AddBlock("parse").Parser(func(c BlockNode) (func(io.Writer, Context) error, error) {
a := c.Args
return func(w io.Writer, c Context) error {
_, err := w.Write([]byte(a))
Expand All @@ -36,7 +36,7 @@ func addRenderTestTags(s Config) {
return err
}, nil
})
s.AddBlock("err2").Parser(func(c ASTBlock) (func(io.Writer, Context) error, error) {
s.AddBlock("err2").Parser(func(c BlockNode) (func(io.Writer, Context) error, error) {
return func(w io.Writer, c Context) error {
return fmt.Errorf("stage 2 error")
}, nil
Expand Down
2 changes: 1 addition & 1 deletion tags/loop.go
Expand Up @@ -31,7 +31,7 @@ func parseLoopExpression(source string) (expression.Expression, error) {
return expr, nil
}

func loopTagParser(node render.ASTBlock) (func(io.Writer, render.Context) error, error) { // nolint: gocyclo
func loopTagParser(node render.BlockNode) (func(io.Writer, render.Context) error, error) { // nolint: gocyclo
expr, err := parseLoopExpression(node.Args)
if err != nil {
return nil, err
Expand Down

0 comments on commit 803471c

Please sign in to comment.