1
0
mirror of https://github.com/golang/go synced 2024-11-24 07:00:13 -07:00

text/template: add break, continue actions in ranges

Adds the two range control actions "break" and "continue". They act the
same as the Go keywords break and continue, but are simplified in that
only the innermost range statement can be broken out of or continued.

Fixes #20531

Change-Id: I4412b3bbfd4dadb0ab74ae718e308c1ac7a0a1e9
Reviewed-on: https://go-review.googlesource.com/66410
Reviewed-by: Rob Pike <r@golang.org>
This commit is contained in:
Tim Cooper 2017-09-26 21:14:03 -03:00 committed by Rob Pike
parent 0b2cb89196
commit 3be5d55180
8 changed files with 193 additions and 31 deletions

View File

@ -110,6 +110,12 @@ data, defined in detail in the corresponding sections that follow.
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.

View File

@ -25,11 +25,12 @@ const maxExecDepth = 100000
// 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.
@ -220,9 +221,17 @@ func (t *Template) DefinedTemplates() string {
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:
@ -233,13 +242,15 @@ func (s *state) walk(dot reflect.Value, node parse.Node) {
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:
@ -247,15 +258,26 @@ func (s *state) walk(dot reflect.Value, node parse.Node) {
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)
@ -264,13 +286,14 @@ func (s *state) walkIfOrWith(typ parse.NodeType, dot reflect.Value, pipe *parse.
}
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,
@ -308,13 +331,14 @@ func isTrue(val reflect.Value) (truth, ok bool) {
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)
@ -323,8 +347,9 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
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:
@ -332,17 +357,23 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
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
@ -353,20 +384,25 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
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) {

View File

@ -513,6 +513,10 @@ var execTests = []execTest{
{"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},

View File

@ -60,6 +60,8 @@ const (
// 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
@ -74,6 +76,8 @@ const (
var key = map[string]itemType{
".": itemDot,
"block": itemBlock,
"break": itemBreak,
"continue": itemContinue,
"define": itemDefine,
"else": itemElse,
"end": itemEnd,

View File

@ -192,7 +192,7 @@ var lexTests = []lexTest{
tRight,
tEOF,
}},
{"keywords", "{{range if else end with}}", []item{
{"keywords", "{{range if else end with break continue}}", []item{
tLeft,
mkItem(itemRange, "range"),
tSpace,
@ -203,6 +203,10 @@ var lexTests = []lexTest{
mkItem(itemEnd, "end"),
tSpace,
mkItem(itemWith, "with"),
tSpace,
mkItem(itemBreak, "break"),
tSpace,
mkItem(itemContinue, "continue"),
tRight,
tEOF,
}},

View File

@ -69,6 +69,8 @@ const (
NodeTemplate // A template invocation action.
NodeVariable // A $ variable.
NodeWith // A with action.
NodeBreak // A break action.
NodeContinue // A continue action.
)
// Nodes.
@ -796,6 +798,68 @@ func (r *RangeNode) Copy() Node {
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

View File

@ -23,12 +23,13 @@ type Tree struct {
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.
@ -219,6 +220,7 @@ func (t *Tree) stopParse() {
t.vars = nil
t.funcs = nil
t.treeSet = nil
t.rangeDepth = 0
}
// Parse parses the template definition string to construct a representation of
@ -373,6 +375,10 @@ func (t *Tree) action() (n Node) {
return t.templateControl()
case itemWith:
return t.withControl()
case itemBreak:
return t.breakControl()
case itemContinue:
return t.continueControl()
}
t.backup()
token := t.peek()
@ -453,7 +459,13 @@ func (t *Tree) parseControl(allowElseIf bool, context string) (pos Pos, line int
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:
@ -498,6 +510,26 @@ func (t *Tree) rangeControl() Node {
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}}

View File

@ -218,6 +218,12 @@ var parseTests = []parseTest{
`{{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,
@ -288,6 +294,12 @@ var parseTests = []parseTest{
{"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{}{