This repository has been archived on 2024-02-28. You can view files and clone it, but cannot push or open issues or pull requests.
stick/exec.go
Tyler Sommer 6a90da394f
Fix default case handling in walkChild
A refreshed understanding has been etched in commentary.
2023-04-07 21:59:24 -06:00

945 lines
22 KiB
Go

package stick
import (
"bytes"
"errors"
"fmt"
"io"
"math"
"regexp"
"strconv"
"strings"
"github.com/tyler-sommer/stick/parse"
)
// Type state represents the internal state of a template execution.
//
// state implements the exported Context interface.
type state struct {
out io.Writer // Output.
node parse.Node // Current node.
name string // The name of the template.
meta *metadata // Additional template metadata.
current *parse.BlockNode // Current block, may be nil.
blocks []map[string]*parse.BlockNode // Block scopes.
macros map[string]*parse.MacroNode // Imported macros.
localMacros map[string]*parse.MacroNode // Macros defined in the current template.
env *Env // The configured Stick environment.
scope *scopeStack // Handles execution scope.
}
// newState creates a new template execution state, ready for use.
func newState(name string, out io.Writer, ctx map[string]Value, env *Env) *state {
return &state{
out: out,
node: nil,
name: name,
meta: &metadata{make(map[string]string)},
blocks: make([]map[string]*parse.BlockNode, 0),
macros: make(map[string]*parse.MacroNode),
localMacros: make(map[string]*parse.MacroNode),
env: env,
scope: &scopeStack{[]map[string]Value{ctx}},
}
}
// A selfValue represents the special `_self` variable.
type selfValue map[string]Value
func (s *state) self() selfValue {
return selfValue{
"templateName": s.name,
"TemplateName": s.name,
}
}
func (s *state) Env() *Env {
return s.env
}
func (s *state) Name() string {
return s.name
}
func (s *state) Scope() ContextScope {
return s.scope
}
func (s *state) Meta() ContextMetadata {
return s.meta
}
// noexport satisfies the Context interface.
func (s *state) noexport() {}
// metadata contains extra information about the state.
//
// metadata implements the exported ContextMetadata interface.
type metadata struct {
attr map[string]string
}
func (m *metadata) All() map[string]string {
ret := make(map[string]string, len(m.attr))
for k, v := range m.attr {
ret[k] = v
}
return ret
}
func (m *metadata) Set(name, val string) {
m.attr[name] = val
}
func (m *metadata) Get(name string) (string, bool) {
ret, ok := m.attr[name]
return ret, ok
}
// noexport satisfies the ContextMetadata interface.
func (m *metadata) noexport() {}
// A scopeStack is a structure that represents local
// and parent scopes.
//
// scopeStack implements the exported ContextScope interface.
type scopeStack struct {
scopes []map[string]Value
}
// push adds a scope on top of the stack.
func (s *scopeStack) push() {
s.scopes = append(s.scopes, make(map[string]Value))
}
// pop removes the top-most scope.
func (s *scopeStack) pop() {
s.scopes = s.scopes[0 : len(s.scopes)-1]
}
// All returns a flat map of the current scope.
func (s *scopeStack) All() map[string]Value {
res := make(map[string]Value)
for _, scope := range s.scopes {
for k, v := range scope {
res[k] = v
}
}
return res
}
// noexport satisfies the ContextScope interface.
func (s *scopeStack) noexport() {}
// Get returns a value in the scope.
//
// This function works from the current (local) scope
// upward. The second parameter returns false if the
// value was not found-- this can be used to distinguish
// a non-existent value and a valid nil value.
func (s *scopeStack) Get(name string) (Value, bool) {
for i := len(s.scopes); i > 0; i-- {
scope := s.scopes[i-1]
if v, ok := scope[name]; ok {
return v, true
}
}
return nil, false
}
// Set sets the value in the scope.
//
// This function will work from the top-most scope downward,
// looking for a scope with name defined. The value is set
// on the scope that it was originally defined on, otherwise
// on the local (last) scope.
func (s *scopeStack) Set(name string, val Value) {
for _, scope := range s.scopes {
if _, ok := scope[name]; ok {
scope[name] = val
return
}
}
s.scopes[len(s.scopes)-1][name] = val
}
// setLocal explicitly sets the value in the local scope.
//
// This is useful when a new scope is created, such as
// a macro call, and you need to override a local variable
// without destroying the value in the parent scope.
//
// fnParam := // an function argument's name
// s.scope.push()
// defer s.scope.pop()
// s.scope.setLocal(fnParam, "some value")
func (s *scopeStack) setLocal(name string, val Value) {
s.scopes[len(s.scopes)-1][name] = val
}
// Method getBlock iterates through each set of blocks, returning the first
// block with the given name.
func (s *state) getBlock(name string) *parse.BlockNode {
for _, blocks := range s.blocks {
if block, ok := blocks[name]; ok {
return block
}
}
return nil
}
func (s *state) getParentBlock(name string) *parse.BlockNode {
rootFound := false
for _, blocks := range s.blocks {
if block, ok := blocks[name]; ok {
if rootFound {
return block
}
rootFound = true
}
}
return nil
}
// Method walk is the main entry-point into template execution.
func (s *state) walk(node parse.Node) error {
switch node := node.(type) {
case *parse.ModuleNode:
if p := node.Parent; p != nil {
tplName, err := s.evalExpr(p.Tpl)
if err != nil {
return err
}
name := CoerceString(tplName)
tree, err := s.env.load(name)
if err != nil {
return err
}
defer func(name string) {
s.name = name
}(s.name)
s.name = name
s.blocks = append(s.blocks, tree.Blocks())
err = s.walkChild(node.BodyNode)
if err != nil {
return err
}
return s.walk(tree.Root())
}
return s.walk(node.BodyNode)
case *parse.BodyNode:
for _, c := range node.All() {
err := s.walk(c)
if err != nil {
return err
}
}
case *parse.MacroNode:
s.localMacros[node.Name] = node
return nil
case *parse.TextNode:
_, err := io.WriteString(s.out, node.Data)
return err
case *parse.PrintNode:
v, err := s.evalExpr(node.X)
if err != nil {
return err
}
_, err = io.WriteString(s.out, CoerceString(v))
return err
case *parse.BlockNode:
name := node.Name
if block := s.getBlock(name); block != nil {
if block.Origin != "" {
defer func(name string) {
s.name = name
}(s.name)
s.name = block.Origin
}
prev := s.current
s.current = block
defer func() {
s.current = prev
}()
return s.walk(block.Body)
}
// TODO: It seems this should never occur.
return errors.New("Unable to locate block " + name)
case *parse.IfNode:
v, err := s.evalExpr(node.Cond)
if err != nil {
return err
}
if CoerceBool(v) {
return s.walk(node.Body)
} else {
return s.walk(node.Else)
}
case *parse.IncludeNode:
tpl, ctx, err := s.walkIncludeNode(node)
if err != nil {
return err
}
err = execute(tpl, s.out, ctx, s.env)
if err != nil {
return err
}
case *parse.EmbedNode:
tpl, ctx, err := s.walkIncludeNode(node.IncludeNode)
if err != nil {
return err
}
si := newState(tpl, s.out, ctx, s.env)
tree, err := s.env.load(tpl)
if err != nil {
return err
}
si.blocks = append(s.blocks, node.Blocks, tree.Blocks())
err = si.walk(tree.Root())
if err != nil {
return err
}
case *parse.UseNode:
return s.walkUseNode(node)
case *parse.ForNode:
return s.walkForNode(node)
case *parse.SetNode:
return s.walkSetNode(node)
case *parse.DoNode:
return s.walkDoNode(node)
case *parse.FilterNode:
return s.walkFilterNode(node)
case *parse.ImportNode:
return s.walkImportNode(node)
case *parse.FromNode:
return s.walkFromNode(node)
case *parse.CommentNode:
// Nothing.
default:
return errors.New("Unknown node " + node.String())
}
return nil
}
// walkChild only executes a subset of nodes, intended to be used on child templates.
func (s *state) walkChild(node parse.Node) error {
switch node := node.(type) {
case *parse.BodyNode:
for _, c := range node.All() {
err := s.walkChild(c)
if err != nil {
return err
}
}
case *parse.UseNode:
return s.walkUseNode(node)
default:
// No need to handle other nodes. This function only populates blocks from a
// referenced template (in a use statement) and does not actually execute anything.
}
return nil
}
func (s *state) walkForNode(node *parse.ForNode) error {
res, err := s.evalExpr(node.X)
if err != nil {
return err
}
kn := node.Key
vn := node.Val
ct, err := Iterate(res, func(k Value, v Value, l Loop) (bool, error) {
s.scope.push()
defer s.scope.pop()
if kn != "" {
s.scope.setLocal(kn, k)
}
s.scope.setLocal(vn, v)
loopValue := map[string]Value{
"Last": l.Last,
"Index": l.Index,
"Index0": l.Index0,
"last": l.Last,
"index": l.Index,
"index0": l.Index0,
"revindex": l.Revindex,
"revindex0": l.Revindex0,
"first": l.First,
"length": l.Length,
}
parent, hasParent := s.scope.Get("loop")
if hasParent {
loopValue["parent"] = parent
}
s.scope.setLocal("loop", loopValue)
err := s.walk(node.Body)
if err != nil {
return true, err
}
return false, nil
})
if err != nil {
return err
}
if ct == 0 {
return s.walk(node.Else)
}
return nil
}
// Method walkInclude determines the necessary parameters for including or embedding a template.
func (s *state) walkIncludeNode(node *parse.IncludeNode) (tpl string, ctx map[string]Value, err error) {
ctx = make(map[string]Value)
v, err := s.evalExpr(node.Tpl)
if err != nil {
return "", nil, err
}
tpl = CoerceString(v)
var with Value
if n := node.With; n != nil {
with, err = s.evalExpr(n)
// TODO: Assert "with" is a hash?
if err != nil {
return "", nil, err
}
}
if !node.Only {
ctx = s.scope.All()
}
if with != nil {
if with, ok := with.(map[string]Value); ok {
for k, v := range with {
ctx[k] = v
}
}
}
return tpl, ctx, err
}
func (s *state) walkUseNode(node *parse.UseNode) error {
v, err := s.evalExpr(node.Tpl)
if err != nil {
return err
}
tpl := CoerceString(v)
tree, err := s.env.load(tpl)
if err != nil {
return err
}
blocks := tree.Blocks()
for orig, alias := range node.Aliases {
v, ok := blocks[orig]
if !ok {
return errors.New("Unable to locate block with name \"" + orig + "\"")
}
blocks[alias] = v
}
l := len(s.blocks)
lb := s.blocks[l-1]
s.blocks = append(append(s.blocks[:l-1], blocks), lb)
return nil
}
func (s *state) walkSetNode(node *parse.SetNode) error {
var v Value
switch node.X.(type) {
case *parse.BodyNode:
// when setting a variable with a body, it may contain any number
// of any type of node or expression. because of this, the value is
// always converted to a string which is stored in the variable name.
prevBuf := s.out
defer func() {
s.out = prevBuf
}()
buf := &bytes.Buffer{}
s.out = buf
err := s.walk(node.X)
if err != nil {
return err
}
v = string(buf.Bytes())
case parse.Expr:
// evaluates the right side of a basic set statement
var err error
v, err = s.evalExpr(node.X)
if err != nil {
return err
}
default:
return fmt.Errorf("unsupported expression type in set statement: %T (bug?)", node.X)
}
s.scope.Set(node.Name, v)
return nil
}
func (s *state) walkDoNode(node *parse.DoNode) error {
_, err := s.evalExpr(node.X)
if err != nil {
return err
}
return nil
}
func (s *state) walkFilterNode(node *parse.FilterNode) error {
prevBuf := s.out
defer func() {
s.out = prevBuf
}()
buf := &bytes.Buffer{}
s.out = buf
err := s.walk(node.Body)
if err != nil {
return err
}
val := string(buf.Bytes())
for _, v := range node.Filters {
f, ok := s.env.Filters[v]
if !ok {
return errors.New("undefined filter \"" + v + "\".")
}
val = CoerceString(f(s, val))
}
io.WriteString(prevBuf, val)
return nil
}
func (s *state) walkImportNode(node *parse.ImportNode) error {
tpl, err := s.evalExpr(node.Tpl)
if err != nil {
return err
}
tree, err := s.env.load(CoerceString(tpl))
if err != nil {
return err
}
macros := make(map[string]macroDef)
for name, def := range tree.Macros() {
macros[name] = macroDef{def}
}
s.scope.Set(node.Alias, macroSet{macros})
return nil
}
func (s *state) walkFromNode(node *parse.FromNode) error {
tpl, err := s.evalExpr(node.Tpl)
if err != nil {
return err
}
tree, err := s.env.load(CoerceString(tpl))
if err != nil {
return err
}
macros := tree.Macros()
for name, alias := range node.Imports {
def, ok := macros[name]
if !ok {
return errors.New("undefined macro " + name)
}
s.macros[alias] = def
}
return nil
}
// Method evalExpr evaluates the given expression, returning a Value or error.
func (s *state) evalExpr(exp parse.Expr) (v Value, e error) {
switch exp := exp.(type) {
case *parse.NullExpr:
return nil, nil
case *parse.BoolExpr:
return exp.Value, nil
case *parse.NameExpr:
if exp.Name == "_self" {
return s.self(), nil
}
if val, ok := s.scope.Get(exp.Name); ok {
v = val
} else {
e = errors.New("undefined variable \"" + exp.Name + "\"")
}
case *parse.NumberExpr:
num, err := strconv.ParseFloat(exp.Value, 64)
if err != nil {
return nil, err
}
return num, nil
case *parse.StringExpr:
return exp.Text, nil
case *parse.GroupExpr:
return s.evalExpr(exp.X)
case *parse.UnaryExpr:
in, err := s.evalExpr(exp.X)
if err != nil {
return nil, err
}
switch exp.Op {
case parse.OpUnaryNot:
return !CoerceBool(in), nil
case parse.OpUnaryPositive:
// no-op, +1 = 1, +(-1) = -1, +(false) = 0
return CoerceNumber(in), nil
case parse.OpUnaryNegative:
return -CoerceNumber(in), nil
}
case *parse.BinaryExpr:
left, err := s.evalExpr(exp.Left)
if err != nil {
return nil, err
}
right, err := s.evalExpr(exp.Right)
if err != nil {
return nil, err
}
switch exp.Op {
case parse.OpBinaryAdd:
return CoerceNumber(left) + CoerceNumber(right), nil
case parse.OpBinarySubtract:
return CoerceNumber(left) - CoerceNumber(right), nil
case parse.OpBinaryMultiply:
return CoerceNumber(left) * CoerceNumber(right), nil
case parse.OpBinaryDivide:
return CoerceNumber(left) / CoerceNumber(right), nil
case parse.OpBinaryFloorDiv:
return math.Floor(CoerceNumber(left) / CoerceNumber(right)), nil
case parse.OpBinaryModulo:
return float64(int(CoerceNumber(left)) % int(CoerceNumber(right))), nil
case parse.OpBinaryPower:
return math.Pow(CoerceNumber(left), CoerceNumber(right)), nil
case parse.OpBinaryConcat:
return CoerceString(left) + CoerceString(right), nil
case parse.OpBinaryEndsWith:
return strings.HasSuffix(CoerceString(left), CoerceString(right)), nil
case parse.OpBinaryStartsWith:
return strings.HasPrefix(CoerceString(left), CoerceString(right)), nil
case parse.OpBinaryIn:
return Contains(right, left)
case parse.OpBinaryNotIn:
res, err := Contains(right, left)
if err != nil {
return false, err
}
return !res, nil
case parse.OpBinaryIs:
if fn, ok := right.(func(v Value) bool); ok {
return fn(left), nil
}
return nil, errors.New("right operand was of unexpected type")
case parse.OpBinaryIsNot:
if fn, ok := right.(func(v Value) bool); ok {
return !fn(left), nil
}
return nil, errors.New("right operand was of unexpected type")
case parse.OpBinaryMatches:
reg, err := regexp.Compile(CoerceString(right))
if err != nil {
return nil, err
}
return reg.MatchString(CoerceString(left)), nil
case parse.OpBinaryEqual:
return Equal(left, right), nil
case parse.OpBinaryNotEqual:
return !Equal(left, right), nil
case parse.OpBinaryGreaterEqual:
return CoerceNumber(left) >= CoerceNumber(right), nil
case parse.OpBinaryGreaterThan:
return CoerceNumber(left) > CoerceNumber(right), nil
case parse.OpBinaryLessEqual:
return CoerceNumber(left) <= CoerceNumber(right), nil
case parse.OpBinaryLessThan:
return CoerceNumber(left) < CoerceNumber(right), nil
case parse.OpBinaryRange:
l, r := CoerceNumber(left), CoerceNumber(right)
res := make([]float64, uint(math.Ceil(r-l))+1)
for i, k := 0, l; k <= r; i, k = i+1, k+1 {
res[i] = k
}
return res, nil
case parse.OpBinaryBitwiseAnd:
return int(CoerceNumber(left)) & int(CoerceNumber(right)), nil
case parse.OpBinaryBitwiseOr:
return int(CoerceNumber(left)) | int(CoerceNumber(right)), nil
case parse.OpBinaryBitwiseXor:
return int(CoerceNumber(left)) ^ int(CoerceNumber(right)), nil
case parse.OpBinaryAnd:
return CoerceBool(left) && CoerceBool(right), nil
case parse.OpBinaryOr:
return CoerceBool(left) || CoerceBool(right), nil
default:
return nil, fmt.Errorf("unsupported binary operator: %s (bug?)", exp.Op)
}
case *parse.FuncExpr:
return s.evalFunction(exp)
case *parse.FilterExpr:
return s.evalFilter(exp)
case *parse.GetAttrExpr:
c, err := s.evalExpr(exp.Cont)
if err != nil {
return nil, err
}
k, err := s.evalExpr(exp.Attr)
if err != nil {
return nil, err
}
exargs := exp.Args
args := make([]Value, len(exargs))
for k, e := range exargs {
v, err := s.evalExpr(e)
if err != nil {
return nil, err
}
args[k] = v
}
if _, ok := c.(selfValue); ok {
if macro, ok := s.localMacros[CoerceString(k)]; ok {
return s.callMacro(macroDef{macro}, args...)
}
// no locally-defined macro defined with the given name, but the
// `_self` variable contains other special values such as `templateName`.
// this will be handled below by the main call to GetAttr.
}
if set, ok := c.(macroSet); ok {
if macro, ok := set.defs[CoerceString(k)]; ok {
return s.callMacro(macro, args...)
}
return nil, errors.New("undefined macro: " + CoerceString(k))
}
v, err = GetAttr(c, k, args...)
if err != nil {
e = err
}
case *parse.TestExpr:
if tfn, ok := s.env.Tests[exp.Name]; ok {
eargs := exp.Args
args := make([]Value, len(eargs))
for i, e := range eargs {
v, err := s.evalExpr(e)
if err != nil {
return nil, err
}
args[i] = v
}
return func(v Value) bool {
return tfn(s, v, args...)
}, nil
}
return nil, fmt.Errorf(`unknown test "%v"`, exp.Name)
case *parse.TernaryIfExpr:
cond, err := s.evalExpr(exp.Cond)
if err != nil {
return nil, err
}
if CoerceBool(cond) == true {
return s.evalExpr(exp.TrueX)
}
return s.evalExpr(exp.FalseX)
case *parse.HashExpr:
vals := make(map[string]Value)
for _, v := range exp.Elements {
var key Value
var err error
if k, ok := v.Key.(*parse.NameExpr); ok {
key = k.Name
} else {
key, err = s.evalExpr(v.Key)
if err != nil {
return nil, err
}
}
val, err := s.evalExpr(v.Value)
if err != nil {
return nil, err
}
vals[CoerceString(key)] = val
}
return vals, nil
case *parse.ArrayExpr:
vals := make([]Value, len(exp.Elements))
for i, v := range exp.Elements {
val, err := s.evalExpr(v)
if err != nil {
return nil, err
}
vals[i] = val
}
return vals, nil
default:
return nil, fmt.Errorf("unable to evaluate unsupported Expr type: %T (bug?)", exp)
}
return v, nil
}
func (s *state) evalFunction(exp *parse.FuncExpr) (Value, error) {
fnName := exp.Name
switch fnName {
case "parent":
if s.current == nil {
return nil, errors.New("not inside a block!")
}
name := s.current.Name
if blk := s.getParentBlock(name); blk != nil {
pout := s.out
buf := &bytes.Buffer{}
s.out = buf
if err := s.walk(blk.Body); err != nil {
return nil, err
}
s.out = pout
return buf.String(), nil
}
return nil, errors.New("Unable to locate block \"" + name + "\"")
case "block":
eargs := exp.Args
if len(eargs) != 1 {
return nil, errors.New("block expects one parameter")
}
val, err := s.evalExpr(eargs[0])
if err != nil {
return nil, err
}
name := CoerceString(val)
if blk := s.getBlock(name); blk != nil {
pout := s.out
buf := &bytes.Buffer{}
s.out = buf
err = s.walk(blk.Body)
if err != nil {
return nil, err
}
s.out = pout
return buf.String(), nil
}
return nil, errors.New("Unable to locate block \"" + name + "\"")
}
if macro, ok := s.macros[fnName]; ok {
eargs := exp.Args
args := make([]Value, len(eargs))
for i, e := range eargs {
v, err := s.evalExpr(e)
if err != nil {
return nil, err
}
args[i] = v
}
return s.callMacro(macroDef{macro}, args...)
}
if fn, ok := s.env.Functions[fnName]; ok {
eargs := exp.Args
args := make([]Value, len(eargs))
for i, e := range eargs {
v, err := s.evalExpr(e)
if err != nil {
return nil, err
}
args[i] = v
}
return fn(s, args...), nil
}
return nil, errors.New("Undeclared function \"" + fnName + "\"")
}
func (s *state) evalFilter(exp *parse.FilterExpr) (Value, error) {
ftName := exp.Name
if fn, ok := s.env.Filters[ftName]; ok {
eargs := exp.Args
if len(eargs) == 0 {
return nil, errors.New("Filter call must receive at least one argument")
}
args := make([]Value, len(eargs))
for i, e := range eargs {
v, err := s.evalExpr(e)
if err != nil {
return nil, err
}
args[i] = v
}
return fn(s, args[0], args[1:]...), nil
}
return nil, errors.New("Undeclared filter \"" + ftName + "\"")
}
type macroDef struct {
*parse.MacroNode
}
type macroSet struct {
defs map[string]macroDef
}
func (s *state) callMacro(macro macroDef, args ...Value) (Value, error) {
s.scope.push()
defer s.scope.pop()
for i, name := range macro.Args {
if i >= len(args) {
s.scope.setLocal(name, nil)
} else {
s.scope.setLocal(name, args[i])
}
}
defer func(buf io.Writer) {
s.out = buf
}(s.out)
buf := &bytes.Buffer{}
s.out = buf
if macro.Origin != "" {
defer func(name string) {
s.name = name
}(s.name)
s.name = macro.Origin
}
err := s.walk(macro.Body)
if err != nil {
return nil, err
}
return buf.String(), nil
}
// execute kicks off execution of the given template.
func execute(name string, out io.Writer, ctx map[string]Value, env *Env) error {
if ctx == nil {
ctx = make(map[string]Value)
}
s := newState(name, out, ctx, env)
tree, err := s.env.load(name)
if err != nil {
return err
}
s.blocks = append(s.blocks, tree.Blocks())
err = s.walk(tree.Root())
if err != nil {
return err
}
return nil
}
// Method load attempts to load and parse the given template.
func (env *Env) load(name string) (*parse.Tree, error) {
tpl, err := env.Loader.Load(name)
if err != nil {
return nil, err
}
tree := parse.NewNamedTree(name, tpl.Contents())
tree.Visitors = append(tree.Visitors, env.Visitors...)
err = tree.Parse()
if err != nil {
return nil, err
}
return tree, nil
}