diff options
| author | Russ Cox <rsc@golang.org> | 2021-05-20 12:46:33 -0400 |
|---|---|---|
| committer | Russ Cox <rsc@golang.org> | 2021-09-23 02:52:10 +0000 |
| commit | d0dd26a88c019d54f22463daae81e785f5867565 (patch) | |
| tree | 95803033c564f840d9778568f7a0dd8f81c07774 /src/text/template/parse | |
| parent | 93453233bd00cc641d2f959f1faf236e0094c2bf (diff) | |
| download | go-d0dd26a88c019d54f22463daae81e785f5867565.tar.xz | |
html/template, text/template: implement break and continue for range loops
Break and continue for range loops was accepted as a proposal in June 2017.
It was implemented in CL 66410 (Oct 2017)
but then rolled back in CL 92155 (Feb 2018)
because html/template changes had not been implemented.
This CL reimplements break and continue in text/template
and then adds support for them in html/template as well.
Fixes #20531.
Change-Id: I05330482a976f1c078b4b49c2287bd9031bb7616
Reviewed-on: https://go-review.googlesource.com/c/go/+/321491
Trust: Russ Cox <rsc@golang.org>
Run-TryBot: Russ Cox <rsc@golang.org>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Rob Pike <r@golang.org>
Diffstat (limited to 'src/text/template/parse')
| -rw-r--r-- | src/text/template/parse/lex.go | 13 | ||||
| -rw-r--r-- | src/text/template/parse/lex_test.go | 2 | ||||
| -rw-r--r-- | src/text/template/parse/node.go | 36 | ||||
| -rw-r--r-- | src/text/template/parse/parse.go | 42 | ||||
| -rw-r--r-- | src/text/template/parse/parse_test.go | 8 |
5 files changed, 99 insertions, 2 deletions
diff --git a/src/text/template/parse/lex.go b/src/text/template/parse/lex.go index 6784071b11..95e33771c0 100644 --- a/src/text/template/parse/lex.go +++ b/src/text/template/parse/lex.go @@ -62,6 +62,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 @@ -76,6 +78,8 @@ const ( var key = map[string]itemType{ ".": itemDot, "block": itemBlock, + "break": itemBreak, + "continue": itemContinue, "define": itemDefine, "else": itemElse, "end": itemEnd, @@ -119,6 +123,8 @@ type lexer struct { parenDepth int // nesting depth of ( ) exprs line int // 1+number of newlines seen startLine int // start line of this item + breakOK bool // break keyword allowed + continueOK bool // continue keyword allowed } // next returns the next rune in the input. @@ -461,7 +467,12 @@ Loop: } switch { case key[word] > itemKeyword: - l.emit(key[word]) + item := key[word] + if item == itemBreak && !l.breakOK || item == itemContinue && !l.continueOK { + l.emit(itemIdentifier) + } else { + l.emit(item) + } case word[0] == '.': l.emit(itemField) case word == "true", word == "false": diff --git a/src/text/template/parse/lex_test.go b/src/text/template/parse/lex_test.go index 6510eed674..df6aabffb2 100644 --- a/src/text/template/parse/lex_test.go +++ b/src/text/template/parse/lex_test.go @@ -35,6 +35,8 @@ var itemName = map[itemType]string{ // keywords itemDot: ".", itemBlock: "block", + itemBreak: "break", + itemContinue: "continue", itemDefine: "define", itemElse: "else", itemIf: "if", diff --git a/src/text/template/parse/node.go b/src/text/template/parse/node.go index 177482f9b2..47268225c8 100644 --- a/src/text/template/parse/node.go +++ b/src/text/template/parse/node.go @@ -71,6 +71,8 @@ const ( NodeVariable // A $ variable. NodeWith // A with action. NodeComment // A comment. + NodeBreak // A break action. + NodeContinue // A continue action. ) // Nodes. @@ -907,6 +909,40 @@ func (i *IfNode) Copy() Node { return i.tr.newIf(i.Pos, i.Line, i.Pipe.CopyPipe(), i.List.CopyList(), i.ElseList.CopyList()) } +// BreakNode represents a {{break}} action. +type BreakNode struct { + tr *Tree + NodeType + Pos + Line int +} + +func (t *Tree) newBreak(pos Pos, line int) *BreakNode { + return &BreakNode{tr: t, NodeType: NodeBreak, Pos: pos, Line: line} +} + +func (b *BreakNode) Copy() Node { return b.tr.newBreak(b.Pos, b.Line) } +func (b *BreakNode) String() string { return "{{break}}" } +func (b *BreakNode) tree() *Tree { return b.tr } +func (b *BreakNode) writeTo(sb *strings.Builder) { sb.WriteString("{{break}}") } + +// ContinueNode represents a {{continue}} action. +type ContinueNode struct { + tr *Tree + NodeType + Pos + Line int +} + +func (t *Tree) newContinue(pos Pos, line int) *ContinueNode { + return &ContinueNode{tr: t, NodeType: NodeContinue, Pos: pos, Line: line} +} + +func (c *ContinueNode) Copy() Node { return c.tr.newContinue(c.Pos, c.Line) } +func (c *ContinueNode) String() string { return "{{continue}}" } +func (c *ContinueNode) tree() *Tree { return c.tr } +func (c *ContinueNode) writeTo(sb *strings.Builder) { sb.WriteString("{{continue}}") } + // RangeNode represents a {{range}} action and its commands. type RangeNode struct { BranchNode diff --git a/src/text/template/parse/parse.go b/src/text/template/parse/parse.go index 1a63961c13..d92bed5d3d 100644 --- a/src/text/template/parse/parse.go +++ b/src/text/template/parse/parse.go @@ -31,6 +31,7 @@ type Tree struct { vars []string // variables defined at the moment. treeSet map[string]*Tree actionLine int // line of left delim starting action + rangeDepth int mode Mode } @@ -224,6 +225,8 @@ func (t *Tree) startParse(funcs []map[string]interface{}, lex *lexer, treeSet ma t.vars = []string{"$"} t.funcs = funcs t.treeSet = treeSet + lex.breakOK = !t.hasFunction("break") + lex.continueOK = !t.hasFunction("continue") } // stopParse terminates parsing. @@ -386,6 +389,10 @@ func (t *Tree) action() (n Node) { switch token := t.nextNonSpace(); token.typ { case itemBlock: return t.blockControl() + case itemBreak: + return t.breakControl(token.pos, token.line) + case itemContinue: + return t.continueControl(token.pos, token.line) case itemElse: return t.elseControl() case itemEnd: @@ -405,6 +412,32 @@ func (t *Tree) action() (n Node) { return t.newAction(token.pos, token.line, t.pipeline("command", itemRightDelim)) } +// Break: +// {{break}} +// Break keyword is past. +func (t *Tree) breakControl(pos Pos, line int) Node { + if token := t.next(); token.typ != itemRightDelim { + t.unexpected(token, "in {{break}}") + } + if t.rangeDepth == 0 { + t.errorf("{{break}} outside {{range}}") + } + return t.newBreak(pos, line) +} + +// Continue: +// {{continue}} +// Continue keyword is past. +func (t *Tree) continueControl(pos Pos, line int) Node { + if token := t.next(); token.typ != itemRightDelim { + t.unexpected(token, "in {{continue}}") + } + if t.rangeDepth == 0 { + t.errorf("{{continue}} outside {{range}}") + } + return t.newContinue(pos, line) +} + // Pipeline: // declarations? command ('|' command)* func (t *Tree) pipeline(context string, end itemType) (pipe *PipeNode) { @@ -480,8 +513,14 @@ func (t *Tree) checkPipeline(pipe *PipeNode, context string) { func (t *Tree) parseControl(allowElseIf bool, context string) (pos Pos, line int, pipe *PipeNode, list, elseList *ListNode) { defer t.popVars(len(t.vars)) pipe = t.pipeline(context, itemRightDelim) + if context == "range" { + t.rangeDepth++ + } var next Node list, next = t.itemList() + if context == "range" { + t.rangeDepth-- + } switch next.Type() { case nodeEnd: //done case nodeElse: @@ -523,7 +562,8 @@ func (t *Tree) ifControl() Node { // {{range pipeline}} itemList {{else}} itemList {{end}} // Range keyword is past. func (t *Tree) rangeControl() Node { - return t.newRange(t.parseControl(false, "range")) + r := t.newRange(t.parseControl(false, "range")) + return r } // With: diff --git a/src/text/template/parse/parse_test.go b/src/text/template/parse/parse_test.go index 9b1be272e5..c3679a08de 100644 --- a/src/text/template/parse/parse_test.go +++ b/src/text/template/parse/parse_test.go @@ -230,6 +230,10 @@ var parseTests = []parseTest{ `{{range $x := .SI}}{{.}}{{end}}`}, {"range 2 vars", "{{range $x, $y := .SI}}{{.}}{{end}}", noError, `{{range $x, $y := .SI}}{{.}}{{end}}`}, + {"range with break", "{{range .SI}}{{.}}{{break}}{{end}}", noError, + `{{range .SI}}{{.}}{{break}}{{end}}`}, + {"range 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, @@ -279,6 +283,10 @@ var parseTests = []parseTest{ {"adjacent args", "{{printf 3`x`}}", hasError, ""}, {"adjacent args with .", "{{printf `x`.}}", hasError, ""}, {"extra end after if", "{{if .X}}a{{else if .Y}}b{{end}}{{end}}", hasError, ""}, + {"break outside range", "{{range .}}{{end}} {{break}}", hasError, ""}, + {"continue outside range", "{{range .}}{{end}} {{continue}}", hasError, ""}, + {"break in range else", "{{range .}}{{else}}{{break}}{{end}}", hasError, ""}, + {"continue in range else", "{{range .}}{{else}}{{continue}}{{end}}", hasError, ""}, // Other kinds of assignments and operators aren't available yet. {"bug0a", "{{$x := 0}}{{$x}}", noError, "{{$x := 0}}{{$x}}"}, {"bug0b", "{{$x += 1}}{{$x}}", hasError, ""}, |
