2017-07-04 17:03:18 +02:00
|
|
|
package render
|
2017-06-25 17:23:20 +02:00
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
2017-07-05 15:51:21 +02:00
|
|
|
"strings"
|
2017-06-28 02:23:09 +02:00
|
|
|
|
2017-07-04 17:12:40 +02:00
|
|
|
"github.com/osteele/liquid/expression"
|
2017-06-25 17:23:20 +02:00
|
|
|
)
|
|
|
|
|
2017-07-05 15:51:21 +02:00
|
|
|
type ParseError string
|
|
|
|
|
|
|
|
func (e ParseError) Error() string { return string(e) }
|
|
|
|
func parseError(format string, a ...interface{}) ParseError {
|
|
|
|
return ParseError(fmt.Sprintf(format, a...))
|
|
|
|
}
|
|
|
|
|
2017-07-02 06:10:54 +02:00
|
|
|
// Parse parses a source template. It returns an AST root, that can be evaluated.
|
2017-07-04 17:08:57 +02:00
|
|
|
func (s Config) Parse(source string) (ASTNode, error) {
|
2017-06-30 22:51:39 +02:00
|
|
|
tokens := Scan(source, "")
|
|
|
|
return s.parseChunks(tokens)
|
|
|
|
}
|
|
|
|
|
2017-06-26 18:41:41 +02:00
|
|
|
// Parse creates an AST from a sequence of Chunks.
|
2017-07-04 17:08:57 +02:00
|
|
|
func (s Config) parseChunks(chunks []Chunk) (ASTNode, error) { // nolint: gocyclo
|
2017-06-30 14:42:11 +02:00
|
|
|
// a stack of control tag state, for matching nested {%if}{%endif%} etc.
|
2017-06-25 17:23:20 +02:00
|
|
|
type frame struct {
|
2017-07-05 15:51:21 +02:00
|
|
|
syntax BlockSyntax
|
|
|
|
node *ASTBlock
|
|
|
|
ap *[]ASTNode
|
2017-06-25 17:23:20 +02:00
|
|
|
}
|
|
|
|
var (
|
2017-07-05 15:51:21 +02:00
|
|
|
g = s.Grammar()
|
2017-07-02 19:31:34 +02:00
|
|
|
root = &ASTSeq{} // root of AST; will be returned
|
|
|
|
ap = &root.Children // newly-constructed nodes are appended here
|
2017-07-05 15:51:21 +02:00
|
|
|
sd BlockSyntax // current block syntax definition
|
|
|
|
bn *ASTBlock // current block node
|
2017-07-02 19:31:34 +02:00
|
|
|
stack []frame // stack of blocks
|
|
|
|
rawTag *ASTRaw // current raw tag
|
2017-06-27 19:47:14 +02:00
|
|
|
inComment = false
|
2017-06-27 23:40:15 +02:00
|
|
|
inRaw = false
|
2017-06-25 17:23:20 +02:00
|
|
|
)
|
|
|
|
for _, c := range chunks {
|
2017-06-27 19:47:14 +02:00
|
|
|
switch {
|
2017-06-30 14:42:11 +02:00
|
|
|
// The parser needs to know about comment and raw, because tags inside
|
|
|
|
// needn't match each other e.g. {%comment%}{%if%}{%endcomment%}
|
|
|
|
// TODO is this true?
|
2017-06-27 19:47:14 +02:00
|
|
|
case inComment:
|
2017-06-29 18:20:16 +02:00
|
|
|
if c.Type == TagChunkType && c.Name == "endcomment" {
|
2017-06-27 19:47:14 +02:00
|
|
|
inComment = false
|
|
|
|
}
|
2017-06-27 23:40:15 +02:00
|
|
|
case inRaw:
|
2017-06-29 18:20:16 +02:00
|
|
|
if c.Type == TagChunkType && c.Name == "endraw" {
|
2017-06-30 14:45:22 +02:00
|
|
|
inRaw = false
|
2017-06-27 23:40:15 +02:00
|
|
|
} else {
|
|
|
|
rawTag.slices = append(rawTag.slices, c.Source)
|
|
|
|
}
|
2017-06-27 19:47:14 +02:00
|
|
|
case c.Type == ObjChunkType:
|
2017-07-04 17:12:40 +02:00
|
|
|
expr, err := expression.Parse(c.Args)
|
2017-06-28 02:23:09 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
*ap = append(*ap, &ASTObject{c, expr})
|
2017-06-27 19:47:14 +02:00
|
|
|
case c.Type == TextChunkType:
|
2017-06-27 17:19:12 +02:00
|
|
|
*ap = append(*ap, &ASTText{Chunk: c})
|
2017-06-27 19:47:14 +02:00
|
|
|
case c.Type == TagChunkType:
|
2017-07-05 15:51:21 +02:00
|
|
|
if cs, ok := g.BlockSyntax(c.Name); ok {
|
2017-06-25 17:23:20 +02:00
|
|
|
switch {
|
2017-06-29 18:20:16 +02:00
|
|
|
case c.Name == "comment":
|
2017-06-27 19:47:14 +02:00
|
|
|
inComment = true
|
2017-06-29 18:20:16 +02:00
|
|
|
case c.Name == "raw":
|
2017-06-27 23:40:15 +02:00
|
|
|
inRaw = true
|
|
|
|
rawTag = &ASTRaw{}
|
|
|
|
*ap = append(*ap, rawTag)
|
2017-07-05 15:51:21 +02:00
|
|
|
case cs.RequiresParent() && (sd == nil || !cs.CanHaveParent(sd)):
|
2017-06-25 17:23:20 +02:00
|
|
|
suffix := ""
|
2017-07-05 15:51:21 +02:00
|
|
|
if sd != nil {
|
|
|
|
suffix = "; immediate parent is " + sd.TagName()
|
2017-06-25 17:23:20 +02:00
|
|
|
}
|
2017-07-05 15:51:21 +02:00
|
|
|
return nil, fmt.Errorf("%s not inside %s%s", c.Name, strings.Join(cs.ParentTags(), " or "), suffix)
|
|
|
|
case cs.IsBlockStart():
|
|
|
|
push := func() {
|
|
|
|
stack = append(stack, frame{syntax: sd, node: bn, ap: ap})
|
|
|
|
sd, bn = cs, &ASTBlock{Chunk: c, syntax: cs}
|
|
|
|
*ap = append(*ap, bn)
|
|
|
|
}
|
|
|
|
push()
|
|
|
|
ap = &bn.Body
|
|
|
|
case cs.IsBranch():
|
|
|
|
n := &ASTBlock{Chunk: c, syntax: cs}
|
|
|
|
bn.Branches = append(bn.Branches, n)
|
2017-06-27 17:39:32 +02:00
|
|
|
ap = &n.Body
|
2017-07-05 15:51:21 +02:00
|
|
|
case cs.IsBlockEnd():
|
|
|
|
pop := func() {
|
|
|
|
f := stack[len(stack)-1]
|
|
|
|
stack = stack[:len(stack)-1]
|
|
|
|
sd, bn, ap = f.syntax, f.node, f.ap
|
|
|
|
}
|
|
|
|
pop()
|
|
|
|
default:
|
|
|
|
panic("unexpected block type")
|
2017-06-25 17:23:20 +02:00
|
|
|
}
|
2017-06-30 22:46:17 +02:00
|
|
|
} else if td, ok := s.FindTagDefinition(c.Name); ok {
|
2017-06-30 23:33:36 +02:00
|
|
|
f, err := td(c.Args)
|
2017-06-26 21:36:05 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2017-06-25 17:23:20 +02:00
|
|
|
}
|
2017-06-28 02:23:09 +02:00
|
|
|
*ap = append(*ap, &ASTFunctional{c, f})
|
2017-06-25 17:23:20 +02:00
|
|
|
} else {
|
2017-06-29 18:20:16 +02:00
|
|
|
return nil, fmt.Errorf("unknown tag: %s", c.Name)
|
2017-06-25 17:23:20 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2017-07-05 15:51:21 +02:00
|
|
|
if bn != nil {
|
|
|
|
return nil, fmt.Errorf("unterminated %s tag at %s", bn.Name, bn.SourceInfo)
|
2017-06-25 17:23:20 +02:00
|
|
|
}
|
2017-06-30 22:46:17 +02:00
|
|
|
if err := s.evaluateBuilders(root); err != nil {
|
2017-06-29 03:45:24 +02:00
|
|
|
return nil, err
|
|
|
|
}
|
2017-06-25 17:23:20 +02:00
|
|
|
if len(root.Children) == 1 {
|
|
|
|
return root.Children[0], nil
|
|
|
|
}
|
|
|
|
return root, nil
|
|
|
|
}
|
2017-06-29 03:45:24 +02:00
|
|
|
|
2017-07-02 05:52:23 +02:00
|
|
|
// nolint: gocyclo
|
2017-07-04 17:08:57 +02:00
|
|
|
func (s Config) evaluateBuilders(n ASTNode) error {
|
2017-06-29 03:45:24 +02:00
|
|
|
switch n := n.(type) {
|
2017-07-03 18:00:43 +02:00
|
|
|
case *ASTBlock:
|
2017-06-29 03:45:24 +02:00
|
|
|
for _, child := range n.Body {
|
2017-06-30 22:46:17 +02:00
|
|
|
if err := s.evaluateBuilders(child); err != nil {
|
2017-06-29 03:45:24 +02:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, branch := range n.Branches {
|
2017-06-30 22:46:17 +02:00
|
|
|
if err := s.evaluateBuilders(branch); err != nil {
|
2017-06-29 03:45:24 +02:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
2017-07-02 19:31:34 +02:00
|
|
|
cd, ok := s.findBlockDef(n.Name)
|
2017-06-29 03:45:24 +02:00
|
|
|
if ok && cd.parser != nil {
|
|
|
|
renderer, err := cd.parser(*n)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
n.renderer = renderer
|
|
|
|
}
|
|
|
|
case *ASTSeq:
|
|
|
|
for _, child := range n.Children {
|
2017-06-30 22:46:17 +02:00
|
|
|
if error := s.evaluateBuilders(child); error != nil {
|
2017-06-29 03:45:24 +02:00
|
|
|
return error
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|