T0 is executed; otherwise, dot is set to the successive elements
of the array, slice, or map and T1 is executed.
+ {{break}}
+ Break out of the surrounding range loop.
+
+ {{continue}}
+ Begin the next iteration of the surrounding range loop.
+
{{template "name"}}
The template with the specified name is executed with nil data.
// template so that multiple executions of the same template
// can execute in parallel.
type state struct {
- tmpl *Template
- wr io.Writer
- node parse.Node // current node, for errors
- vars []variable // push-down stack of variable values.
- depth int // the height of the stack of executing templates.
+ tmpl *Template
+ wr io.Writer
+ node parse.Node // current node, for errors.
+ vars []variable // push-down stack of variable values.
+ depth int // the height of the stack of executing templates.
+ rangeDepth int // nesting level of range loops.
}
// variable holds the dynamic value of a variable such as $, $x etc.
return s
}
+type rangeControl int8
+
+const (
+ rangeNone rangeControl = iota // no action.
+ rangeBreak // break out of range.
+ rangeContinue // continues next range iteration.
+)
+
// Walk functions step through the major pieces of the template structure,
// generating output as they go.
-func (s *state) walk(dot reflect.Value, node parse.Node) {
+func (s *state) walk(dot reflect.Value, node parse.Node) rangeControl {
s.at(node)
switch node := node.(type) {
case *parse.ActionNode:
s.printValue(node, val)
}
case *parse.IfNode:
- s.walkIfOrWith(parse.NodeIf, dot, node.Pipe, node.List, node.ElseList)
+ return s.walkIfOrWith(parse.NodeIf, dot, node.Pipe, node.List, node.ElseList)
case *parse.ListNode:
for _, node := range node.Nodes {
- s.walk(dot, node)
+ if c := s.walk(dot, node); c != rangeNone {
+ return c
+ }
}
case *parse.RangeNode:
- s.walkRange(dot, node)
+ return s.walkRange(dot, node)
case *parse.TemplateNode:
s.walkTemplate(dot, node)
case *parse.TextNode:
s.writeError(err)
}
case *parse.WithNode:
- s.walkIfOrWith(parse.NodeWith, dot, node.Pipe, node.List, node.ElseList)
+ return s.walkIfOrWith(parse.NodeWith, dot, node.Pipe, node.List, node.ElseList)
+ case *parse.BreakNode:
+ if s.rangeDepth == 0 {
+ s.errorf("invalid break outside of range")
+ }
+ return rangeBreak
+ case *parse.ContinueNode:
+ if s.rangeDepth == 0 {
+ s.errorf("invalid continue outside of range")
+ }
+ return rangeContinue
default:
s.errorf("unknown node: %s", node)
}
+ return rangeNone
}
// walkIfOrWith walks an 'if' or 'with' node. The two control structures
// are identical in behavior except that 'with' sets dot.
-func (s *state) walkIfOrWith(typ parse.NodeType, dot reflect.Value, pipe *parse.PipeNode, list, elseList *parse.ListNode) {
+func (s *state) walkIfOrWith(typ parse.NodeType, dot reflect.Value, pipe *parse.PipeNode, list, elseList *parse.ListNode) rangeControl {
defer s.pop(s.mark())
val := s.evalPipeline(dot, pipe)
truth, ok := isTrue(val)
}
if truth {
if typ == parse.NodeWith {
- s.walk(val, list)
+ return s.walk(val, list)
} else {
- s.walk(dot, list)
+ return s.walk(dot, list)
}
} else if elseList != nil {
- s.walk(dot, elseList)
+ return s.walk(dot, elseList)
}
+ return rangeNone
}
// IsTrue reports whether the value is 'true', in the sense of not the zero of its type,
return truth, true
}
-func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
+func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) rangeControl {
s.at(r)
defer s.pop(s.mark())
val, _ := indirect(s.evalPipeline(dot, r.Pipe))
// mark top of stack before any variables in the body are pushed.
mark := s.mark()
- oneIteration := func(index, elem reflect.Value) {
+ s.rangeDepth++
+ oneIteration := func(index, elem reflect.Value) rangeControl {
// Set top var (lexically the second if there are two) to the element.
if len(r.Pipe.Decl) > 0 {
s.setVar(1, elem)
if len(r.Pipe.Decl) > 1 {
s.setVar(2, index)
}
- s.walk(elem, r.List)
+ ctrl := s.walk(elem, r.List)
s.pop(mark)
+ return ctrl
}
switch val.Kind() {
case reflect.Array, reflect.Slice:
break
}
for i := 0; i < val.Len(); i++ {
- oneIteration(reflect.ValueOf(i), val.Index(i))
+ if ctrl := oneIteration(reflect.ValueOf(i), val.Index(i)); ctrl == rangeBreak {
+ break
+ }
}
- return
+ s.rangeDepth--
+ return rangeNone
case reflect.Map:
if val.Len() == 0 {
break
}
for _, key := range sortKeys(val.MapKeys()) {
- oneIteration(key, val.MapIndex(key))
+ if ctrl := oneIteration(key, val.MapIndex(key)); ctrl == rangeBreak {
+ break
+ }
}
- return
+ s.rangeDepth--
+ return rangeNone
case reflect.Chan:
if val.IsNil() {
break
if !ok {
break
}
- oneIteration(reflect.ValueOf(i), elem)
+ if ctrl := oneIteration(reflect.ValueOf(i), elem); ctrl == rangeBreak {
+ break
+ }
}
if i == 0 {
break
}
- return
+ s.rangeDepth--
+ return rangeNone
case reflect.Invalid:
break // An invalid value is likely a nil map, etc. and acts like an empty map.
default:
s.errorf("range can't iterate over %v", val)
}
+ s.rangeDepth--
if r.ElseList != nil {
- s.walk(dot, r.ElseList)
+ return s.walk(dot, r.ElseList)
}
+ return rangeNone
}
func (s *state) walkTemplate(dot reflect.Value, t *parse.TemplateNode) {
{"declare in range", "{{range $x := .PSI}}<{{$foo:=$x}}{{$x}}>{{end}}", "<21><22><23>", tVal, true},
{"range count", `{{range $i, $x := count 5}}[{{$i}}]{{$x}}{{end}}`, "[0]a[1]b[2]c[3]d[4]e", tVal, true},
{"range nil count", `{{range $i, $x := count 0}}{{else}}empty{{end}}`, "empty", tVal, true},
+ {"range quick break", `{{range .SI}}{{break}}{{.}}{{end}}`, "", tVal, true},
+ {"range break after two", `{{range $i, $x := .SI}}{{if ge $i 2}}{{break}}{{end}}{{.}}{{end}}`, "34", tVal, true},
+ {"range continue", `{{range .SI}}{{continue}}{{.}}{{end}}`, "", tVal, true},
+ {"range continue condition", `{{range .SI}}{{if eq . 3 }}{{continue}}{{end}}{{.}}{{end}}`, "45", tVal, true},
// Cute examples.
{"or as if true", `{{or .SI "slice is empty"}}`, "[3 4 5]", tVal, true},
// Keywords appear after all the rest.
itemKeyword // used only to delimit the keywords
itemBlock // block keyword
+ itemBreak // break keyword
+ itemContinue // continue keyword
itemDot // the cursor, spelled '.'
itemDefine // define keyword
itemElse // else keyword
var key = map[string]itemType{
".": itemDot,
"block": itemBlock,
+ "break": itemBreak,
+ "continue": itemContinue,
"define": itemDefine,
"else": itemElse,
"end": itemEnd,
tRight,
tEOF,
}},
- {"keywords", "{{range if else end with}}", []item{
+ {"keywords", "{{range if else end with break continue}}", []item{
tLeft,
mkItem(itemRange, "range"),
tSpace,
mkItem(itemEnd, "end"),
tSpace,
mkItem(itemWith, "with"),
+ tSpace,
+ mkItem(itemBreak, "break"),
+ tSpace,
+ mkItem(itemContinue, "continue"),
tRight,
tEOF,
}},
NodeTemplate // A template invocation action.
NodeVariable // A $ variable.
NodeWith // A with action.
+ NodeBreak // A break action.
+ NodeContinue // A continue action.
)
// Nodes.
return r.tr.newRange(r.Pos, r.Line, r.Pipe.CopyPipe(), r.List.CopyList(), r.ElseList.CopyList())
}
+// BreakNode represents a {{break}} action.
+type BreakNode struct {
+ NodeType
+ Pos
+ tr *Tree
+}
+
+func (t *Tree) newBreak(pos Pos) *BreakNode {
+ return &BreakNode{NodeType: NodeBreak, Pos: pos, tr: t}
+}
+
+func (b *BreakNode) Type() NodeType {
+ return b.NodeType
+}
+
+func (b *BreakNode) String() string {
+ return "{{break}}"
+}
+
+func (b *BreakNode) Copy() Node {
+ return b.tr.newBreak(b.Pos)
+}
+
+func (b *BreakNode) Position() Pos {
+ return b.Pos
+}
+
+func (b *BreakNode) tree() *Tree {
+ return b.tr
+}
+
+// ContinueNode represents a {{continue}} action.
+type ContinueNode struct {
+ NodeType
+ Pos
+ tr *Tree
+}
+
+func (t *Tree) newContinue(pos Pos) *ContinueNode {
+ return &ContinueNode{NodeType: NodeContinue, Pos: pos, tr: t}
+}
+
+func (c *ContinueNode) Type() NodeType {
+ return c.NodeType
+}
+
+func (c *ContinueNode) String() string {
+ return "{{continue}}"
+}
+
+func (c *ContinueNode) Copy() Node {
+ return c.tr.newContinue(c.Pos)
+}
+
+func (c *ContinueNode) Position() Pos {
+ return c.Pos
+}
+
+func (c *ContinueNode) tree() *Tree {
+ return c.tr
+}
+
// WithNode represents a {{with}} action and its commands.
type WithNode struct {
BranchNode
Root *ListNode // top-level root of the tree.
text string // text parsed to create the template (or its parent)
// Parsing only; cleared after parse.
- funcs []map[string]interface{}
- lex *lexer
- token [3]item // three-token lookahead for parser.
- peekCount int
- vars []string // variables defined at the moment.
- treeSet map[string]*Tree
+ funcs []map[string]interface{}
+ lex *lexer
+ token [3]item // three-token lookahead for parser.
+ peekCount int
+ vars []string // variables defined at the moment.
+ treeSet map[string]*Tree
+ rangeDepth int // nesting level of range loops.
}
// Copy returns a copy of the Tree. Any parsing state is discarded.
t.vars = nil
t.funcs = nil
t.treeSet = nil
+ t.rangeDepth = 0
}
// Parse parses the template definition string to construct a representation of
return t.templateControl()
case itemWith:
return t.withControl()
+ case itemBreak:
+ return t.breakControl()
+ case itemContinue:
+ return t.continueControl()
}
t.backup()
token := t.peek()
defer t.popVars(len(t.vars))
pipe = t.pipeline(context)
var next Node
+ if context == "range" {
+ t.rangeDepth++
+ }
list, next = t.itemList()
+ if context == "range" {
+ t.rangeDepth--
+ }
switch next.Type() {
case nodeEnd: //done
case nodeElse:
return t.newRange(t.parseControl(false, "range"))
}
+// Break:
+// {{break}}
+// Break keyword is past.
+func (t *Tree) breakControl() Node {
+ if t.rangeDepth == 0 {
+ t.errorf("unexpected break outside of range")
+ }
+ return t.newBreak(t.expect(itemRightDelim, "break").pos)
+}
+
+// Continue:
+// {{continue}}
+// Continue keyword is past.
+func (t *Tree) continueControl() Node {
+ if t.rangeDepth == 0 {
+ t.errorf("unexpected continue outside of range")
+ }
+ return t.newContinue(t.expect(itemRightDelim, "continue").pos)
+}
+
// With:
// {{with pipeline}} itemList {{end}}
// {{with pipeline}} itemList {{else}} itemList {{end}}
`{{range $x := .SI}}{{.}}{{end}}`},
{"range 2 vars", "{{range $x, $y := .SI}}{{.}}{{end}}", noError,
`{{range $x, $y := .SI}}{{.}}{{end}}`},
+ {"range []int with break", "{{range .SI}}{{break}}{{.}}{{end}}", noError,
+ `{{range .SI}}{{break}}{{.}}{{end}}`},
+ {"range []int with break in else", "{{range .SI}}{{range .SI}}{{.}}{{else}}{{break}}{{end}}{{end}}", noError,
+ `{{range .SI}}{{range .SI}}{{.}}{{else}}{{break}}{{end}}{{end}}`},
+ {"range []int with continue", "{{range .SI}}{{continue}}{{.}}{{end}}", noError,
+ `{{range .SI}}{{continue}}{{.}}{{end}}`},
{"constants", "{{range .SI 1 -3.2i true false 'a' nil}}{{end}}", noError,
`{{range .SI 1 -3.2i true false 'a' nil}}{{end}}`},
{"template", "{{template `x`}}", noError,
{"empty pipeline", `{{printf "%d" ( ) }}`, hasError, ""},
// Missing pipeline in block
{"block definition", `{{block "foo"}}hello{{end}}`, hasError, ""},
+ // Invalid range control
+ {"break outside of range", `{{break}}`, hasError, ""},
+ {"break in range else, outside of range", `{{range .}}{{.}}{{else}}{{break}}{{end}}`, hasError, ""},
+ {"continue outside of range", `{{continue}}`, hasError, ""},
+ {"continue in range else, outside of range", `{{range .}}{{.}}{{else}}{{continue}}{{end}}`, hasError, ""},
+ {"additional break data", `{{range .}}{{break label}}{{end}}`, hasError, ""},
}
var builtins = map[string]interface{}{