Skip to content

text/template: support range-over-func #68329

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 14 commits into from
Closed
1 change: 1 addition & 0 deletions doc/next/6-stdlib/99-minor/text/template/66107.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Templates now support range-over-func.
6 changes: 4 additions & 2 deletions src/text/template/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,15 +98,17 @@ data, defined in detail in the corresponding sections that follow.
{{if pipeline}} T1 {{else}}{{if pipeline}} T0 {{end}}{{end}}

{{range pipeline}} T1 {{end}}
The value of the pipeline must be an array, slice, map, or channel.
The value of the pipeline must be an array, slice, map, iter.Seq,
iter.Seq2 or channel.
If the value of the pipeline has length zero, nothing is output;
otherwise, dot is set to the successive elements of the array,
slice, or map and T1 is executed. If the value is a map and the
keys are of basic type with a defined order, the elements will be
visited in sorted key order.

{{range pipeline}} T1 {{else}} T0 {{end}}
The value of the pipeline must be an array, slice, map, or channel.
The value of the pipeline must be an array, slice, map, iter.Seq,
iter.Seq2 or channel.
If the value of the pipeline has length zero, dot is unaffected and
T0 is executed; otherwise, dot is set to the successive elements
of the array, slice, or map and T1 is executed.
Expand Down
37 changes: 37 additions & 0 deletions src/text/template/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,43 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
return
case reflect.Invalid:
break // An invalid value is likely a nil map, etc. and acts like an empty map.
case reflect.Func:
if val.Type().CanSeq() {
if len(r.Pipe.Decl) > 1 {
s.errorf("can't use %s iterate over more than one variable", val)
break
}
run := false
for v := range val.Seq() {
run = true
// Pass element as second value,
// as we do for channels.
oneIteration(reflect.Value{}, v)
}
if !run {
break
}
return
}
if val.Type().CanSeq2() {
run := false
for i, v := range val.Seq2() {
run = true
if len(r.Pipe.Decl) > 1 {
oneIteration(i, v)
} else {
// If there is only one range variable,
// oneIteration will use the
// second value.
oneIteration(reflect.Value{}, i)
}
}
if !run {
break
}
return
}
fallthrough
default:
s.errorf("range can't iterate over %v", val)
}
Expand Down
32 changes: 32 additions & 0 deletions src/text/template/exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"flag"
"fmt"
"io"
"iter"
"reflect"
"strings"
"sync"
Expand Down Expand Up @@ -601,6 +602,17 @@ 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 iter.Seq[int]", `{{range $i := .}}{{$i}}{{end}}`, "01", fVal1(2), true},
{"i = range iter.Seq[int]", `{{$i := 0}}{{range $i = .}}{{$i}}{{end}}`, "01", fVal1(2), true},
{"range iter.Seq[int] over two var", `{{range $i, $c := .}}{{$c}}{{end}}`, "", fVal1(2), false},
{"i, c := range iter.Seq2[int,int]", `{{range $i, $c := .}}{{$i}}{{$c}}{{end}}`, "0112", fVal2(2), true},
{"i, c = range iter.Seq2[int,int]", `{{$i := 0}}{{$c := 0}}{{range $i, $c = .}}{{$i}}{{$c}}{{end}}`, "0112", fVal2(2), true},
{"i = range iter.Seq2[int,int]", `{{$i := 0}}{{range $i = .}}{{$i}}{{end}}`, "01", fVal2(2), true},
{"i := range iter.Seq2[int,int]", `{{range $i := .}}{{$i}}{{end}}`, "01", fVal2(2), true},
{"i,c,x range iter.Seq2[int,int]", `{{$i := 0}}{{$c := 0}}{{$x := 0}}{{range $i, $c = .}}{{$i}}{{$c}}{{end}}`, "0112", fVal2(2), true},
{"i,x range iter.Seq[int]", `{{$i := 0}}{{$x := 0}}{{range $i = .}}{{$i}}{{end}}`, "01", fVal1(2), true},
{"range iter.Seq[int] else", `{{range $i := .}}{{$i}}{{else}}empty{{end}}`, "empty", fVal1(0), true},
{"range iter.Seq2[int,int] else", `{{range $i := .}}{{$i}}{{else}}empty{{end}}`, "empty", fVal2(0), true},

// Cute examples.
{"or as if true", `{{or .SI "slice is empty"}}`, "[3 4 5]", tVal, true},
Expand Down Expand Up @@ -705,6 +717,26 @@ var execTests = []execTest{
{"issue60801", "{{$k := 0}}{{$v := 0}}{{range $k, $v = .AI}}{{$k}}={{$v}} {{end}}", "0=3 1=4 2=5 ", tVal, true},
}

func fVal1(i int) iter.Seq[int] {
return func(yield func(int) bool) {
for v := range i {
if !yield(v) {
break
}
}
}
}

func fVal2(i int) iter.Seq2[int, int] {
return func(yield func(int, int) bool) {
for v := range i {
if !yield(v, v+1) {
break
}
}
}
}

func zeroArgs() string {
return "zeroArgs"
}
Expand Down