Repository: CloudyKit/jet
Pull request #131 (2020-01-22 15:28:31)

Add return keyword & exec() built-in function
This will allow returning a value from a template and using that value in the calling template, enabling users to have "function" templates they can reuse.

foo.jet:
```
{{ f := "f" }}
{{ o := "o" }}

... some content that will be discarded when this template runs inside exec() ...

{{ return f+o+o }}
```

test.jet:
```
{{ foo := exec("./foo") }}

<p>below the line, there will only be "foo":</p>
<hr>
{{ foo }}
```

annismckenzie: Uuuuh, this looks neat! Can you add some unit tests?
sauerbraten: Here you go!
annismckenzie: After we merge this it'd be nice if you added a couple lines of documentation to the wiki for this feature. I'll arrange access for you.
sauerbraten: I'd love to, but I don't have permission to do that :D
No write access 🤷‍♂ 
annismckenzie: I think it's time – there you go. 😬
sauerbraten: Is there a policy whether to merge as-is or can/should I squash-merge?
annismckenzie: Always use `Rebase and merge`, please. 👍
annismckenzie: No squashing. I like keeping the history.

Patch:
diff --git a/constructors.go b/constructors.go
index f6d20e9..e8364ee 100644
--- a/constructors.go
+++ b/constructors.go
@@ -134,6 +134,10 @@ func (t *Template) newInclude(pos Pos, line int, name, pipe Expression) *Include
 	return &IncludeNode{NodeBase: NodeBase{TemplateName: t.Name, NodeType: NodeInclude, Pos: pos, Line: line}, Name: name, Expression: pipe}
 }
 
+func (t *Template) newReturn(pos Pos, line int, pipe Expression) *ReturnNode {
+	return &ReturnNode{NodeBase: NodeBase{TemplateName: t.Name, NodeType: NodeReturn, Pos: pos, Line: line}, Value: pipe}
+}
+
 func (t *Template) newNumber(pos Pos, text string, typ itemType) (*NumberNode, error) {
 	n := &NumberNode{NodeBase: NodeBase{TemplateName: t.Name, NodeType: NodeNumber, Pos: pos}, Text: text}
 	// todo: optimize
diff --git a/default.go b/default.go
index fc26062..33217bb 100644
--- a/default.go
+++ b/default.go
@@ -19,6 +19,7 @@ import (
 	"fmt"
 	"html"
 	"io"
+	"io/ioutil"
 	"net/url"
 	"reflect"
 	"strings"
@@ -111,6 +112,36 @@ func init() {
 
 			return hiddenTRUE
 		})),
+		"exec": reflect.ValueOf(Func(func(a Arguments) (result reflect.Value) {
+			a.RequireNumOfArguments("exec", 1, 2)
+			t, err := a.runtime.set.GetTemplate(a.Get(0).String())
+			if err != nil {
+				panic(err)
+			}
+
+			a.runtime.newScope()
+			w := a.runtime.Writer
+			a.runtime.Writer = ioutil.Discard
+			a.runtime.blocks = t.processedBlocks
+			root := t.Root
+			if t.extends != nil {
+				root = t.extends.Root
+			}
+
+			if a.NumOfArguments() > 1 {
+				c := a.runtime.context
+				a.runtime.context = a.Get(1)
+				result = a.runtime.executeList(root)
+				a.runtime.context = c
+			} else {
+				result = a.runtime.executeList(root)
+			}
+
+			a.runtime.releaseScope()
+			a.runtime.Writer = w
+
+			return result
+		})),
 	}
 }
 
diff --git a/eval.go b/eval.go
index b4fe5ad..0845580 100644
--- a/eval.go
+++ b/eval.go
@@ -372,8 +372,15 @@ func (st *Runtime) executeYieldBlock(block *BlockNode, blockParam, yieldParam *B
 	}
 }
 
-func (st *Runtime) executeList(list *ListNode) {
+func (st *Runtime) executeList(list *ListNode) reflect.Value {
 	inNewSCOPE := false
+	defer func() {
+		if inNewSCOPE {
+			st.releaseScope()
+		}
+	}()
+
+	returnValue := reflect.Value{}
 
 	for i := 0; i < len(list.Nodes); i++ {
 		node := list.Nodes[i]
@@ -425,9 +432,9 @@ func (st *Runtime) executeList(list *ListNode) {
 			}
 
 			if castBoolean(st.evalPrimaryExpressionGroup(node.Expression)) {
-				st.executeList(node.List)
+				returnValue = st.executeList(node.List)
 			} else if node.ElseList != nil {
-				st.executeList(node.ElseList)
+				returnValue = st.executeList(node.ElseList)
 			}
 			if isLet {
 				st.releaseScope()
@@ -456,7 +463,7 @@ func (st *Runtime) executeList(list *ListNode) {
 			ranger := getRanger(expression)
 			indexValue, rangeValue, end := ranger.Range()
 			if !end {
-				for !end {
+				for !end && !returnValue.IsValid() {
 					if isSet {
 						if isLet {
 							if isKeyVal {
@@ -476,11 +483,11 @@ func (st *Runtime) executeList(list *ListNode) {
 					} else {
 						st.context = rangeValue
 					}
-					st.executeList(node.List)
+					returnValue = st.executeList(node.List)
 					indexValue, rangeValue, end = ranger.Range()
 				}
 			} else if node.ElseList != nil {
-				st.executeList(node.ElseList)
+				returnValue = st.executeList(node.ElseList)
 			}
 			st.context = context
 			if isLet {
@@ -535,17 +542,19 @@ func (st *Runtime) executeList(list *ListNode) {
 					t = t.extends
 					Root = t.Root
 				}
-				st.executeList(Root)
+				returnValue = st.executeList(Root)
 				st.releaseScope()
 				if node.Expression != nil {
 					st.context = context
 				}
 			}
+		case NodeReturn:
+			node := node.(*ReturnNode)
+			returnValue = st.evalPrimaryExpressionGroup(node.Value)
 		}
 	}
-	if inNewSCOPE {
-		st.releaseScope()
-	}
+
+	return returnValue
 }
 
 var (
@@ -566,7 +575,7 @@ func (st *Runtime) evalPrimaryExpressionGroup(node Expression) reflect.Value {
 	case NodeLogicalExpr:
 		return st.evalLogicalExpression(node.(*LogicalExprNode))
 	case NodeNotExpr:
-		return boolValue(!castBoolean(st.evalPrimaryExpressionGroup(node.(*NotExprNode).Expr)))
+		return reflect.ValueOf(!castBoolean(st.evalPrimaryExpressionGroup(node.(*NotExprNode).Expr)))
 	case NodeTernaryExpr:
 		node := node.(*TernaryExprNode)
 		if castBoolean(st.evalPrimaryExpressionGroup(node.Boolean)) {
@@ -835,7 +844,7 @@ func (st *Runtime) evalNumericComparativeExpression(node *NumericComparativeExpr
 			node.Left.errorf("a non numeric value in numeric comparative expression")
 		}
 	}
-	return boolValue(isTrue)
+	return reflect.ValueOf(isTrue)
 }
 
 func (st *Runtime) evalLogicalExpression(node *LogicalExprNode) reflect.Value {
@@ -845,22 +854,16 @@ func (st *Runtime) evalLogicalExpression(node *LogicalExprNode) reflect.Value {
 	} else {
 		isTrue = isTrue || castBoolean(st.evalPrimaryExpressionGroup(node.Right))
 	}
-	return boolValue(isTrue)
-}
-
-func boolValue(isTrue bool) reflect.Value {
-	if isTrue {
-		return valueBoolTRUE
-	}
-	return valueBoolFALSE
+	return reflect.ValueOf(isTrue)
 }
 
 func (st *Runtime) evalComparativeExpression(node *ComparativeExprNode) reflect.Value {
 	left, right := st.evalPrimaryExpressionGroup(node.Left), st.evalPrimaryExpressionGroup(node.Right)
+	equal := checkEquality(left, right)
 	if node.Operator.typ == itemNotEquals {
-		return boolValue(!checkEquality(left, right))
+		return reflect.ValueOf(!equal)
 	}
-	return boolValue(checkEquality(left, right))
+	return reflect.ValueOf(equal)
 }
 
 func toInt(v reflect.Value) int64 {
diff --git a/lex.go b/lex.go
index 09f9dbe..4b9ce39 100644
--- a/lex.go
+++ b/lex.go
@@ -87,6 +87,7 @@ const (
 	itemExtends
 	itemBlock
 	itemYield
+	itemReturn
 	itemContent
 	itemInclude
 	itemElse
@@ -109,6 +110,7 @@ var key = map[string]itemType{
 	"include": itemInclude,
 	"block":   itemBlock,
 	"yield":   itemYield,
+	"return":  itemReturn,
 
 	"else": itemElse,
 	"end":  itemEnd,
diff --git a/node.go b/node.go
index bd7412e..df22cc9 100644
--- a/node.go
+++ b/node.go
@@ -89,6 +89,7 @@ const (
 	NodeInclude
 	NodeYield
 	NodeSet
+	NodeReturn
 	beginExpressions
 	NodeString //A string constant.
 	NodeNil    //An untyped nil constant.
@@ -664,3 +665,12 @@ func (s *SliceExprNode) String() string {
 	}
 	return fmt.Sprintf("%s[%s:%s]", s.Base, index_string, len_string)
 }
+
+type ReturnNode struct {
+	NodeBase
+	Value Expression
+}
+
+func (n *ReturnNode) String() string {
+	return fmt.Sprintf("return %v", n.Value)
+}
diff --git a/parse.go b/parse.go
index 4b0c1a3..a84d27f 100644
--- a/parse.go
+++ b/parse.go
@@ -431,6 +431,12 @@ func (t *Template) parseInclude() Node {
 	return t.newInclude(name.Position(), t.lex.lineNumber(), name, pipe)
 }
 
+func (t *Template) parseReturn() Node {
+	value := t.expression("return")
+	t.expect(itemRightDelim, "return")
+	return t.newReturn(value.Position(), t.lex.lineNumber(), value)
+}
+
 // itemListBlock:
 //	textOrAction*
 // Terminates at {{end}} or {{else}}, returned separately.
@@ -497,6 +503,8 @@ func (t *Template) action() (n Node) {
 		return t.parseInclude()
 	case itemYield:
 		return t.parseYield()
+	case itemReturn:
+		return t.parseReturn()
 	}
 
 	t.backup()


Test patch:
diff --git a/eval_test.go b/eval_test.go
index d9dc5f7..4a965f1 100644
--- a/eval_test.go
+++ b/eval_test.go
@@ -605,6 +605,20 @@ func TestSet_Parse(t *testing.T) {
 	group.Wait()
 }
 
+func TestExecReturn(t *testing.T) {
+	set := NewHTMLSet("./testData/execReturn")
+	RunJetTestWithSet(t, set, nil, nil, "simple", "", "\n\n... some content that will be discarded when this template runs inside exec() ...\n\n")
+	RunJetTestWithSet(t, set, nil, nil, "test_simple", "", "foo\n")
+	RunJetTestWithSet(t, set, nil, nil, "in_if", "", "\n\n\n")
+	RunJetTestWithSet(t, set, nil, nil, "test_in_if", "", "from inside if branch\n")
+	context := map[string]interface{}{"arr": []string{"foo", "bar"}}
+	RunJetTestWithSet(t, set, nil, context, "in_range", "", "\n0: foo\n\n\n")
+	RunJetTestWithSet(t, set, nil, context, "test_in_range", "", "from inside if branch inside range\n")
+	RunJetTestWithSet(t, set, nil, nil, "included", "", "... some content that will be discarded when this template runs inside exec() ...\n\n")
+	RunJetTestWithSet(t, set, nil, nil, "in_include", "", "bla bla\n... some content that will be discarded when this template runs inside exec() ...\n\n\nfoo\n")
+	RunJetTestWithSet(t, set, nil, nil, "test_in_include", "", "from inside included template\n")
+}
+
 func BenchmarkSimpleAction(b *testing.B) {
 	t, _ := JetTestingSet.GetTemplate("actionNode_dummy")
 	for i := 0; i < b.N; i++ {
diff --git a/testData/execReturn/in_if.jet b/testData/execReturn/in_if.jet
new file mode 100644
index 0000000..4d91590
--- /dev/null
+++ b/testData/execReturn/in_if.jet
@@ -0,0 +1,3 @@
+{{if true}}
+{{return "from inside if branch"}}
+{{end}}
diff --git a/testData/execReturn/in_include.jet b/testData/execReturn/in_include.jet
new file mode 100644
index 0000000..65c20f2
--- /dev/null
+++ b/testData/execReturn/in_include.jet
@@ -0,0 +1,3 @@
+bla bla
+{{ include "./included.jet" }}
+foo
diff --git a/testData/execReturn/in_range.jet b/testData/execReturn/in_range.jet
new file mode 100644
index 0000000..6334e53
--- /dev/null
+++ b/testData/execReturn/in_range.jet
@@ -0,0 +1,4 @@
+{{ range i, v := .arr }}
+{{ i }}: {{ v }}
+{{ if v == "foo" }}{{ return "from inside if branch inside range" }}{{ end }}
+{{ end }}
diff --git a/testData/execReturn/included.jet b/testData/execReturn/included.jet
new file mode 100644
index 0000000..0d8bc55
--- /dev/null
+++ b/testData/execReturn/included.jet
@@ -0,0 +1,2 @@
+... some content that will be discarded when this template runs inside exec() ...
+{{ return "from inside included template" }}
diff --git a/testData/execReturn/simple.jet b/testData/execReturn/simple.jet
new file mode 100644
index 0000000..ff5f19f
--- /dev/null
+++ b/testData/execReturn/simple.jet
@@ -0,0 +1,4 @@
+{{ f := "f" }}
+{{ o := "o" }}
+... some content that will be discarded when this template runs inside exec() ...
+{{ return f+o+o }}
diff --git a/testData/execReturn/test_in_if.jet b/testData/execReturn/test_in_if.jet
new file mode 100644
index 0000000..37a0f0d
--- /dev/null
+++ b/testData/execReturn/test_in_if.jet
@@ -0,0 +1,1 @@
+{{ exec("./in_if.jet") }}
diff --git a/testData/execReturn/test_in_include.jet b/testData/execReturn/test_in_include.jet
new file mode 100644
index 0000000..e3e1b7d
--- /dev/null
+++ b/testData/execReturn/test_in_include.jet
@@ -0,0 +1,1 @@
+{{ exec("./in_include") }}
diff --git a/testData/execReturn/test_in_range.jet b/testData/execReturn/test_in_range.jet
new file mode 100644
index 0000000..8a0eca9
--- /dev/null
+++ b/testData/execReturn/test_in_range.jet
@@ -0,0 +1,1 @@
+{{ exec("./in_range.jet", .) }}
diff --git a/testData/execReturn/test_simple.jet b/testData/execReturn/test_simple.jet
new file mode 100644
index 0000000..1e642cc
--- /dev/null
+++ b/testData/execReturn/test_simple.jet
@@ -0,0 +1,1 @@
+{{ foo := exec("./simple") }}{{ foo }}