Skip to content

Commit

Permalink
feat: add break, continue actions in ranges
Browse files Browse the repository at this point in the history
This is copied verbatim from https://go-review.googlesource.com/c/go/+/66410/. The above commit was reverted due to issues with html/template (see golang/go#23683
). However, as we don't have such issues, there seems to be no issue in adding it to this fork. Changes pass all tests.
  • Loading branch information
jo3-l committed Dec 11, 2020
1 parent fadaefc commit d8fc99c
Show file tree
Hide file tree
Showing 7 changed files with 172 additions and 26 deletions.
74 changes: 55 additions & 19 deletions exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type state struct {
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.
operations int

parent *state
Expand Down Expand Up @@ -247,9 +248,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:
Expand All @@ -260,29 +269,42 @@ 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:
if _, err := s.wr.Write(node.Text); err != nil {
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)
Expand All @@ -291,13 +313,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,
Expand Down Expand Up @@ -335,15 +358,16 @@ 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.incrOPs(1)

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 {
s.incrOPs(1)

// Set top var (lexically the second if there are two) to the element.
Expand All @@ -354,26 +378,33 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
if len(r.Pipe.Decl) > 1 {
s.setTopVar(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:
if val.Len() == 0 {
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
Expand All @@ -384,20 +415,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) {
Expand Down
4 changes: 4 additions & 0 deletions exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,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},
Expand Down
4 changes: 4 additions & 0 deletions parse/lex.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,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
Expand All @@ -75,6 +77,8 @@ const (
var key = map[string]itemType{
".": itemDot,
"block": itemBlock,
"break": itemBreak,
"continue": itemContinue,
"define": itemDefine,
"else": itemElse,
"end": itemEnd,
Expand Down
6 changes: 5 additions & 1 deletion parse/lex_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -203,6 +203,10 @@ var lexTests = []lexTest{
mkItem(itemEnd, "end"),
tSpace,
mkItem(itemWith, "with"),
tSpace,
mkItem(itemBreak, "break"),
tSpace,
mkItem(itemContinue, "continue"),
tRight,
tEOF,
}},
Expand Down
54 changes: 54 additions & 0 deletions parse/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -798,6 +800,58 @@ 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
Expand Down
44 changes: 38 additions & 6 deletions parse/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -461,7 +467,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:
Expand Down Expand Up @@ -506,6 +518,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}}
Expand Down

0 comments on commit d8fc99c

Please sign in to comment.