mirror of
https://github.com/danog/liquid.git
synced 2025-01-22 13:53:09 +01:00
Render tree is distinct type from parse AST
This commit is contained in:
parent
8f63cb781e
commit
803471c15f
@ -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
|
||||
|
@ -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
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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))
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ package render
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// A CompilationError is a parse error during template compilation.
|
||||
@ -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)
|
||||
@ -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
|
||||
if cd.parser != nil {
|
||||
r, err := cd.parser(*n)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
renderer = r
|
||||
}
|
||||
return &ASTBlock{
|
||||
node := BlockNode{
|
||||
Chunk: n.Chunk,
|
||||
renderer: renderer,
|
||||
syntax: n.syntax,
|
||||
Body: body,
|
||||
Branches: branches,
|
||||
}, nil
|
||||
}
|
||||
if cd.parser != nil {
|
||||
r, err := cd.parser(node)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
node.renderer = r
|
||||
}
|
||||
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 {
|
||||
|
@ -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")
|
||||
})
|
||||
}
|
||||
|
@ -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{})
|
||||
@ -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.
|
||||
@ -81,8 +81,8 @@ 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.
|
||||
@ -90,7 +90,7 @@ 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) {
|
||||
|
@ -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)
|
||||
@ -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))
|
||||
}
|
||||
@ -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
|
||||
|
47
render/render_node.go
Normal file
47
render/render_node.go
Normal file
@ -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
|
||||
}
|
@ -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))
|
||||
@ -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
|
||||
|
@ -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
|
||||
|
12
tags/tags.go
12
tags/tags.go
@ -37,7 +37,7 @@ func assignTag(source string) (func(io.Writer, render.Context) error, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func captureTagParser(node render.ASTBlock) (func(io.Writer, render.Context) error, error) {
|
||||
func captureTagParser(node render.BlockNode) (func(io.Writer, render.Context) error, error) {
|
||||
// TODO verify syntax
|
||||
varname := node.Args
|
||||
return func(w io.Writer, ctx render.Context) error {
|
||||
@ -50,7 +50,7 @@ func captureTagParser(node render.ASTBlock) (func(io.Writer, render.Context) err
|
||||
}, nil
|
||||
}
|
||||
|
||||
func caseTagParser(node render.ASTBlock) (func(io.Writer, render.Context) error, error) {
|
||||
func caseTagParser(node render.BlockNode) (func(io.Writer, render.Context) error, error) {
|
||||
// TODO parse error on non-empty node.Body
|
||||
// TODO case can include an else
|
||||
expr, err := expression.Parse(node.Args)
|
||||
@ -59,7 +59,7 @@ func caseTagParser(node render.ASTBlock) (func(io.Writer, render.Context) error,
|
||||
}
|
||||
type caseRec struct {
|
||||
expr expression.Expression
|
||||
node *render.ASTBlock
|
||||
node *render.BlockNode
|
||||
}
|
||||
cases := []caseRec{}
|
||||
for _, branch := range node.Branches {
|
||||
@ -87,11 +87,11 @@ func caseTagParser(node render.ASTBlock) (func(io.Writer, render.Context) error,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ifTagParser(polarity bool) func(render.ASTBlock) (func(io.Writer, render.Context) error, error) { // nolint: gocyclo
|
||||
return func(node render.ASTBlock) (func(io.Writer, render.Context) error, error) {
|
||||
func ifTagParser(polarity bool) func(render.BlockNode) (func(io.Writer, render.Context) error, error) { // nolint: gocyclo
|
||||
return func(node render.BlockNode) (func(io.Writer, render.Context) error, error) {
|
||||
type branchRec struct {
|
||||
test expression.Expression
|
||||
body *render.ASTBlock
|
||||
body *render.BlockNode
|
||||
}
|
||||
expr, err := expression.Parse(node.Args)
|
||||
if err != nil {
|
||||
|
Loading…
x
Reference in New Issue
Block a user