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

Add the beginnings of the template execution code. Lots still to do,

including evaluation up the data tree (in this code all fields must be
in dot itself), plus more control structure, but the basics are in place.

R=rsc, r
CC=golang-dev
https://golang.org/cl/4665041
This commit is contained in:
Rob Pike 2011-06-28 23:04:08 +10:00
parent 5c15f8710c
commit 81592c298b
7 changed files with 587 additions and 55 deletions

View File

@ -6,6 +6,7 @@ include ../../../Make.inc
TARG=template
GOFILES=\
exec.go\
lex.go\
parse.go\

View File

@ -0,0 +1,316 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package template
import (
"fmt"
"io"
"os"
"reflect"
)
// state represents the state of an execution. It's not part of the
// template so that multiple executions of the same template
// can execute in parallel.
type state struct {
tmpl *Template
wr io.Writer
line int // line number for errors
}
// errorf formats the error and terminates processing.
func (s *state) errorf(format string, args ...interface{}) {
format = fmt.Sprintf("template: %s:%d: %s", s.tmpl.name, s.line, format)
panic(fmt.Errorf(format, args...))
}
// error terminates processing.
func (s *state) error(err os.Error) {
s.errorf("%s", err)
}
// Execute applies a parsed template to the specified data object,
// writing the output to wr.
func (t *Template) Execute(wr io.Writer, data interface{}) (err os.Error) {
defer t.recover(&err)
state := &state{
tmpl: t,
wr: wr,
line: 1,
}
if t.root == nil {
state.errorf("must be parsed before execution")
}
state.walk(reflect.ValueOf(data), t.root)
return
}
// Walk functions step through the major pieces of the template structure,
// generating output as they go.
func (s *state) walk(data reflect.Value, n node) {
switch n := n.(type) {
case *actionNode:
s.line = n.line
s.printValue(n, s.evalPipeline(data, n.pipeline))
case *listNode:
for _, node := range n.nodes {
s.walk(data, node)
}
case *rangeNode:
s.walkRange(data, n)
case *textNode:
if _, err := s.wr.Write(n.text); err != nil {
s.error(err)
}
default:
s.errorf("unknown node: %s", n)
}
}
func (s *state) walkRange(data reflect.Value, r *rangeNode) {
val := s.evalPipeline(data, r.pipeline)
switch val.Kind() {
case reflect.Array, reflect.Slice:
if val.Len() == 0 {
break
}
for i := 0; i < val.Len(); i++ {
s.walk(val.Index(i), r.list)
}
return
case reflect.Map:
if val.Len() == 0 {
break
}
for _, key := range val.MapKeys() {
s.walk(val.MapIndex(key), r.list)
}
return
default:
s.errorf("range can't iterate over value of type %T", val.Interface())
}
if r.elseList != nil {
s.walk(data, r.elseList)
}
}
// Eval functions evaluate pipelines, commands, and their elements and extract
// values from the data structure by examining fields, calling methods, and so on.
// The printing of those values happens only through walk functions.
func (s *state) evalPipeline(data reflect.Value, pipe []*commandNode) reflect.Value {
value := reflect.Value{}
for _, cmd := range pipe {
value = s.evalCommand(data, cmd, value) // previous value is this one's final arg.
}
return value
}
func (s *state) evalCommand(data reflect.Value, cmd *commandNode, final reflect.Value) reflect.Value {
switch field := cmd.args[0].(type) {
case *dotNode:
if final.IsValid() {
s.errorf("can't give argument to dot")
}
return data
case *fieldNode:
return s.evalFieldNode(data, field, cmd.args, final)
}
s.errorf("%s not a field", cmd.args[0])
panic("not reached")
}
func (s *state) evalFieldNode(data reflect.Value, field *fieldNode, args []node, final reflect.Value) reflect.Value {
// Up to the last entry, it must be a field.
n := len(field.ident)
for i := 0; i < n-1; i++ {
data = s.evalField(data, field.ident[i])
}
// Now it can be a field or method and if a method, gets arguments.
return s.evalMethodOrField(data, field.ident[n-1], args, final)
}
func (s *state) evalField(data reflect.Value, fieldName string) reflect.Value {
for {
if data.Kind() != reflect.Ptr {
break
}
data = reflect.Indirect(data)
}
switch data.Kind() {
case reflect.Struct:
// Is it a field?
field := data.FieldByName(fieldName)
// TODO: look higher up the tree if we can't find it here. Also unexported fields
// might succeed higher up, as map keys.
if field.IsValid() && field.Type().PkgPath() == "" { // valid and exported
return field
}
s.errorf("%s has no field %s", data.Type(), fieldName)
default:
s.errorf("can't evaluate field %s of type %s", fieldName, data.Type())
}
panic("not reached")
}
func (s *state) evalMethodOrField(data reflect.Value, fieldName string, args []node, final reflect.Value) reflect.Value {
ptr := data
for data.Kind() == reflect.Ptr {
ptr, data = data, reflect.Indirect(data)
}
// Is it a method? We use the pointer because it has value methods too.
// TODO: reflect.Type could use a MethodByName.
for i := 0; i < ptr.Type().NumMethod(); i++ {
method := ptr.Type().Method(i)
if method.Name == fieldName {
return s.evalMethod(ptr, i, args, final)
}
}
if len(args) > 1 || final.IsValid() {
s.errorf("%s is not a method but has arguments", fieldName)
}
switch data.Kind() {
case reflect.Struct:
return s.evalField(data, fieldName)
default:
s.errorf("can't handle evaluation of field %s of type %s", fieldName, data.Type())
}
panic("not reached")
}
var (
osErrorType = reflect.TypeOf(new(os.Error)).Elem()
)
func (s *state) evalMethod(v reflect.Value, i int, args []node, final reflect.Value) reflect.Value {
method := v.Type().Method(i)
typ := method.Type
fun := method.Func
numIn := len(args)
if final.IsValid() {
numIn++
}
if !typ.IsVariadic() && numIn < typ.NumIn()-1 || !typ.IsVariadic() && numIn != typ.NumIn() {
s.errorf("wrong number of args for %s: want %d got %d", method.Name, typ.NumIn(), len(args))
}
// We allow methods with 1 result or 2 results where the second is an os.Error.
switch {
case typ.NumOut() == 1:
case typ.NumOut() == 2 && typ.Out(1) == osErrorType:
default:
s.errorf("can't handle multiple results from method %q", method.Name)
}
// Build the arg list.
argv := make([]reflect.Value, numIn)
// First arg is the receiver.
argv[0] = v
// Others must be evaluated.
for i := 1; i < len(args); i++ {
argv[i] = s.evalArg(v, typ.In(i), args[i])
}
// Add final value if necessary.
if final.IsValid() {
argv[len(args)] = final
}
result := fun.Call(argv)
// If we have an os.Error that is not nil, stop execution and return that error to the caller.
if len(result) == 2 && !result[1].IsNil() {
s.error(result[1].Interface().(os.Error))
}
return result[0]
}
func (s *state) evalArg(data reflect.Value, typ reflect.Type, n node) reflect.Value {
if field, ok := n.(*fieldNode); ok {
value := s.evalFieldNode(data, field, []node{n}, reflect.Value{})
if !value.Type().AssignableTo(typ) {
s.errorf("wrong type for value; expected %s; got %s", typ, value.Type())
}
return value
}
switch typ.Kind() {
// TODO: boolean
case reflect.String:
return s.evalString(data, typ, n)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return s.evalInteger(data, typ, n)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return s.evalUnsignedInteger(data, typ, n)
case reflect.Float32, reflect.Float64:
return s.evalFloat(data, typ, n)
case reflect.Complex64, reflect.Complex128:
return s.evalComplex(data, typ, n)
}
s.errorf("can't handle node %s for method arg of type %s", n, typ)
panic("not reached")
}
func (s *state) evalString(v reflect.Value, typ reflect.Type, n node) reflect.Value {
if n, ok := n.(*stringNode); ok {
value := reflect.New(typ).Elem()
value.SetString(n.text)
return value
}
s.errorf("expected string; found %s", n)
panic("not reached")
}
func (s *state) evalInteger(v reflect.Value, typ reflect.Type, n node) reflect.Value {
if n, ok := n.(*numberNode); ok && n.isInt {
value := reflect.New(typ).Elem()
value.SetInt(n.int64)
return value
}
s.errorf("expected integer; found %s", n)
panic("not reached")
}
func (s *state) evalUnsignedInteger(v reflect.Value, typ reflect.Type, n node) reflect.Value {
if n, ok := n.(*numberNode); ok && n.isUint {
value := reflect.New(typ).Elem()
value.SetUint(n.uint64)
return value
}
s.errorf("expected unsigned integer; found %s", n)
panic("not reached")
}
func (s *state) evalFloat(v reflect.Value, typ reflect.Type, n node) reflect.Value {
if n, ok := n.(*numberNode); ok && n.isFloat && !n.imaginary {
value := reflect.New(typ).Elem()
value.SetFloat(n.float64)
return value
}
s.errorf("expected float; found %s", n)
panic("not reached")
}
func (s *state) evalComplex(v reflect.Value, typ reflect.Type, n node) reflect.Value {
if n, ok := n.(*numberNode); ok && n.isFloat && n.imaginary {
value := reflect.New(typ).Elem()
value.SetComplex(complex(0, n.float64))
return value
}
s.errorf("expected complex; found %s", n)
panic("not reached")
}
// printValue writes the textual representation of the value to the output of
// the template.
func (s *state) printValue(n node, v reflect.Value) {
if !v.IsValid() {
return
}
switch v.Kind() {
case reflect.Ptr:
if v.IsNil() {
s.errorf("%s: nil value", n)
}
case reflect.Chan, reflect.Func, reflect.Interface:
s.errorf("can't print %s of type %s", n, v.Type())
}
fmt.Fprint(s.wr, v.Interface())
}

View File

@ -0,0 +1,160 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package template
import (
"bytes"
"fmt"
"os"
"sort"
"strings"
"testing"
)
// T has lots of interesting pieces to use to test execution.
type T struct {
// Basics
I int
U16 uint16
X string
// Nested structs.
U *U
// Slices
SI []int
SEmpty []int
// Maps
MSI map[string]int
MSIEmpty map[string]int
}
// Simple methods with and without arguments.
func (t *T) Method0() string {
return "resultOfMethod0"
}
func (t *T) Method1(a int) int {
return a
}
func (t *T) Method2(a uint16, b string) string {
return fmt.Sprintf("Method2: %d %s", a, b)
}
func (t *T) MAdd(a int, b []int) []int {
v := make([]int, len(b))
for i, x := range b {
v[i] = x + a
}
return v
}
// MSort is used to sort map keys for stable output. (Nice trick!)
func (t *T) MSort(m map[string]int) []string {
keys := make([]string, len(m))
i := 0
for k := range m {
keys[i] = k
i++
}
sort.SortStrings(keys)
return keys
}
// EPERM returns a value and an os.Error according to its argument.
func (t *T) EPERM(a int) (int, os.Error) {
if a == 0 {
return 0, os.EPERM
}
return a, nil
}
type U struct {
V string
}
var tVal = &T{
I: 17,
U16: 16,
X: "x",
U: &U{"v"},
SI: []int{3, 4, 5},
MSI: map[string]int{"one": 1, "two": 2, "three": 3},
}
type execTest struct {
name string
input string
output string
data interface{}
ok bool
}
var execTests = []execTest{
{"empty", "", "", nil, true},
{"text", "some text", "some text", nil, true},
{".X", "-{{.X}}-", "-x-", tVal, true},
{".U.V", "-{{.U.V}}-", "-v-", tVal, true},
{".Method0", "-{{.Method0}}-", "-resultOfMethod0-", tVal, true},
{".Method1(1234)", "-{{.Method1 1234}}-", "-1234-", tVal, true},
{".Method1(.I)", "-{{.Method1 .I}}-", "-17-", tVal, true},
{".Method2(3, .X)", "-{{.Method2 3 .X}}-", "-Method2: 3 x-", tVal, true},
{".Method2(.U16, `str`)", "-{{.Method2 .U16 `str`}}-", "-Method2: 16 str-", tVal, true},
{"pipeline", "-{{.Method0 | .Method2 .U16}}-", "-Method2: 16 resultOfMethod0-", tVal, true},
{"range []int", "{{range .SI}}-{{.}}-{{end}}", "-3--4--5-", tVal, true},
{"range empty no else", "{{range .SEmpty}}-{{.}}-{{end}}", "", tVal, true},
{"range []int else", "{{range .SI}}-{{.}}-{{else}}EMPTY{{end}}", "-3--4--5-", tVal, true},
{"range empty else", "{{range .SEmpty}}-{{.}}-{{else}}EMPTY{{end}}", "EMPTY", tVal, true},
{"range []int method", "{{range .SI | .MAdd .I}}-{{.}}-{{end}}", "-20--21--22-", tVal, true},
{"range map", "{{range .MSI | .MSort}}-{{.}}-{{end}}", "-one--three--two-", tVal, true},
{"range empty map no else", "{{range .MSIEmpty}}-{{.}}-{{end}}", "", tVal, true},
{"range map else", "{{range .MSI | .MSort}}-{{.}}-{{else}}EMPTY{{end}}", "-one--three--two-", tVal, true},
{"range empty map else", "{{range .MSIEmpty}}-{{.}}-{{else}}EMPTY{{end}}", "EMPTY", tVal, true},
{"error method, no error", "{{.EPERM 1}}", "1", tVal, true},
{"error method, error", "{{.EPERM 0}}", "1", tVal, false},
}
func TestExecute(t *testing.T) {
b := new(bytes.Buffer)
for _, test := range execTests {
tmpl := New(test.name)
err := tmpl.Parse(test.input)
if err != nil {
t.Errorf("%s: parse error: %s", test.name, err)
continue
}
b.Reset()
err = tmpl.Execute(b, test.data)
switch {
case !test.ok && err == nil:
t.Errorf("%s: expected error; got none", test.name)
continue
case test.ok && err != nil:
t.Errorf("%s: unexpected execute error: %s", test.name, err)
continue
case !test.ok && err != nil:
continue
}
result := b.String()
if result != test.output {
t.Errorf("%s: expected\n\t%q\ngot\n\t%q", test.name, test.output, result)
}
}
}
// Check that an error from a method flows back to the top.
func TestExecuteError(t *testing.T) {
b := new(bytes.Buffer)
tmpl := New("error")
err := tmpl.Parse("{{.EPERM 0}}")
if err != nil {
t.Fatalf("parse error: %s", err)
}
err = tmpl.Execute(b, tVal)
if err == nil {
t.Errorf("expected error; got none")
} else if !strings.Contains(err.String(), os.EPERM.String()) {
t.Errorf("expected os.EPERM; got %s %s", err)
}
}

View File

@ -39,7 +39,7 @@ const (
itemEOF
itemElse // else keyword
itemEnd // end keyword
itemField // alphanumeric identifier, starting with '.'.
itemField // alphanumeric identifier, starting with '.', possibly chained ('.x.y')
itemIdentifier // alphanumeric identifier
itemIf // if keyword
itemLeftMeta // left meta-string
@ -273,7 +273,9 @@ Loop:
for {
switch r := l.next(); {
case isAlphaNumeric(r):
// absorb
// absorb.
case r == '.' && l.input[l.start] == '.':
// field chaining; absorb into one token.
default:
l.backup()
word := l.input[l.start:l.pos]

View File

@ -46,13 +46,18 @@ var lexTests = []lexTest{
tRight,
tEOF,
}},
{"dots", "{{.x . .2 .x.y }}", []item{
{"dot", "{{.}}", []item{
tLeft,
{itemDot, "."},
tRight,
tEOF,
}},
{"dots", "{{.x . .2 .x.y}}", []item{
tLeft,
{itemField, ".x"},
{itemDot, "."},
{itemNumber, ".2"},
{itemField, ".x"},
{itemField, ".y"},
{itemField, ".x.y"},
tRight,
tEOF,
}},

View File

@ -10,13 +10,14 @@ import (
"os"
"runtime"
"strconv"
"strings"
)
// Template is the representation of a parsed template.
type Template struct {
// TODO: At the moment, these are all internal to parsing.
name string
root *listNode
name string
root *listNode
// Parsing.
lex *lexer
tokens chan item
token item // token lookahead for parser
@ -64,6 +65,7 @@ const (
nodeText nodeType = iota
nodeAction
nodeCommand
nodeDot
nodeElse
nodeEnd
nodeField
@ -103,11 +105,11 @@ func (l *listNode) String() string {
// textNode holds plain text.
type textNode struct {
nodeType
text string
text []byte
}
func newText(text string) *textNode {
return &textNode{nodeType: nodeText, text: text}
return &textNode{nodeType: nodeText, text: []byte(text)}
}
func (t *textNode) String() string {
@ -117,11 +119,12 @@ func (t *textNode) String() string {
// actionNode holds an action (something bounded by metacharacters).
type actionNode struct {
nodeType
line int
pipeline []*commandNode
}
func newAction() *actionNode {
return &actionNode{nodeType: nodeAction}
func newAction(line int, pipeline []*commandNode) *actionNode {
return &actionNode{nodeType: nodeAction, line: line, pipeline: pipeline}
}
func (a *actionNode) append(command *commandNode) {
@ -164,18 +167,35 @@ func (i *identifierNode) String() string {
return fmt.Sprintf("I=%s", i.ident)
}
// fieldNode holds a field (identifier starting with '.'). The period is dropped from the ident.
// dotNode holds the special identifier '.'. It is represented by a nil pointer.
type dotNode bool
func newDot() *dotNode {
return nil
}
func (d *dotNode) typ() nodeType {
return nodeDot
}
func (d *dotNode) String() string {
return "{{<.>}}"
}
// fieldNode holds a field (identifier starting with '.').
// The names may be chained ('.x.y').
// The period is dropped from each ident.
type fieldNode struct {
nodeType
ident string
ident []string
}
func newField(ident string) *fieldNode {
return &fieldNode{nodeType: nodeField, ident: ident[1:]} //drop period
return &fieldNode{nodeType: nodeField, ident: strings.Split(ident[1:], ".")} // [1:] to drop leading period
}
func (f *fieldNode) String() string {
return fmt.Sprintf("F=.%s", f.ident)
return fmt.Sprintf("F=%s", f.ident)
}
// numberNode holds a number, signed or unsigned, integer, floating, or imaginary.
@ -283,11 +303,14 @@ func (e *endNode) String() string {
return "{{end}}"
}
// elseNode represents an {{else}} action. It is represented by a nil pointer.
type elseNode bool
// elseNode represents an {{else}} action.
type elseNode struct {
nodeType
line int
}
func newElse() *elseNode {
return nil
func newElse(line int) *elseNode {
return &elseNode{nodeType: nodeElse, line: line}
}
func (e *elseNode) typ() nodeType {
@ -298,23 +321,24 @@ func (e *elseNode) String() string {
return "{{else}}"
}
// rangeNode represents an {{range}} action and its commands.
// rangeNode represents a {{range}} action and its commands.
type rangeNode struct {
nodeType
field node
line int
pipeline []*commandNode
list *listNode
elseList *listNode
}
func newRange(field node, list *listNode) *rangeNode {
return &rangeNode{nodeType: nodeRange, field: field, list: list}
func newRange(line int, pipeline []*commandNode, list *listNode) *rangeNode {
return &rangeNode{nodeType: nodeRange, line: line, pipeline: pipeline, list: list}
}
func (r *rangeNode) String() string {
if r.elseList != nil {
return fmt.Sprintf("({{range %s}} %s {{else}} %s)", r.field, r.list, r.elseList)
return fmt.Sprintf("({{range %s}} %s {{else}} %s)", r.pipeline, r.list, r.elseList)
}
return fmt.Sprintf("({{range %s}} %s)", r.field, r.list)
return fmt.Sprintf("({{range %s}} %s)", r.pipeline, r.list)
}
// Parsing.
@ -351,24 +375,31 @@ func (t *Template) unexpected(token item, context string) {
t.errorf("unexpected %s in %s", token, context)
}
// Parse parses the template definition string and constructs an efficient representation of the template.
// recover is the handler that turns panics into returns from the top
// level of Parse or Execute.
func (t *Template) recover(errp *os.Error) {
e := recover()
if e != nil {
if _, ok := e.(runtime.Error); ok {
panic(e)
}
t.root, t.lex, t.tokens = nil, nil, nil
*errp = e.(os.Error)
}
return
}
// Parse parses the template definition string to construct an internal representation
// of the template for execution.
func (t *Template) Parse(s string) (err os.Error) {
t.lex, t.tokens = lex(t.name, s)
defer func() {
e := recover()
if e != nil {
if _, ok := e.(runtime.Error); ok {
panic(e)
}
err = e.(os.Error)
}
return
}()
defer t.recover(&err)
var next node
t.root, next = t.itemList(true)
if next != nil {
t.errorf("unexpected %s", next)
}
t.lex, t.tokens = nil, nil
return nil
}
@ -410,8 +441,8 @@ func (t *Template) textOrAction() node {
// control
// command ("|" command)*
// Left meta is past. Now get actions.
// First word could be a keyword such as range.
func (t *Template) action() (n node) {
action := newAction()
switch token := t.next(); token.typ {
case itemRange:
return t.rangeControl()
@ -421,23 +452,29 @@ func (t *Template) action() (n node) {
return t.endControl()
}
t.backup()
Loop:
return newAction(t.lex.lineNumber(), t.pipeline("command"))
}
// Pipeline:
// field or command
// pipeline "|" pipeline
func (t *Template) pipeline(context string) (pipe []*commandNode) {
for {
switch token := t.next(); token.typ {
case itemRightMeta:
break Loop
case itemIdentifier, itemField:
return
case itemIdentifier, itemField, itemDot:
t.backup()
cmd, err := t.command()
if err != nil {
t.error(err)
}
action.append(cmd)
pipe = append(pipe, cmd)
default:
t.unexpected(token, "command")
t.unexpected(token, context)
}
}
return action
return
}
// Range:
@ -445,10 +482,9 @@ Loop:
// {{range field}} itemList {{else}} itemList {{end}}
// Range keyword is past.
func (t *Template) rangeControl() node {
field := t.expect(itemField, "range")
t.expect(itemRightMeta, "range")
pipeline := t.pipeline("range")
list, next := t.itemList(false)
r := newRange(newField(field.val), list)
r := newRange(t.lex.lineNumber(), pipeline, list)
switch next.typ() {
case nodeEnd: //done
case nodeElse:
@ -474,7 +510,7 @@ func (t *Template) endControl() node {
// Else keyword is past.
func (t *Template) elseControl() node {
t.expect(itemRightMeta, "else")
return newElse()
return newElse(t.lex.lineNumber())
}
// command:
@ -494,6 +530,8 @@ Loop:
return nil, os.NewError(token.val)
case itemIdentifier:
cmd.append(newIdentifier(token.val))
case itemDot:
cmd.append(newDot())
case itemField:
cmd.append(newField(token.val))
case itemNumber:
@ -518,5 +556,8 @@ Loop:
t.unexpected(token, "command")
}
}
if len(cmd.args) == 0 {
t.errorf("empty command")
}
return cmd, nil
}

View File

@ -127,6 +127,8 @@ var parseTests = []parseTest{
`[(text: "some text")]`},
{"emptyMeta", "{{}}", noError,
`[(action: [])]`},
{"field", "{{.X}}", noError,
`[(action: [(command: [F=[X]])])]`},
{"simple command", "{{hello}}", noError,
`[(action: [(command: [I=hello])])]`},
{"multi-word command", "{{hello world}}", noError,
@ -137,15 +139,20 @@ var parseTests = []parseTest{
"[(action: [(command: [I=hello S=`quoted text`])])]"},
{"pipeline", "{{hello|world}}", noError,
`[(action: [(command: [I=hello]) (command: [I=world])])]`},
{"simple range", "{{range .x}}hello{{end}}", noError,
`[({{range F=.x}} [(text: "hello")])]`},
{"nested range", "{{range .x}}hello{{range .y}}goodbye{{end}}{{end}}", noError,
`[({{range F=.x}} [(text: "hello")({{range F=.y}} [(text: "goodbye")])])]`},
{"range with else", "{{range .x}}true{{else}}false{{end}}", noError,
`[({{range F=.x}} [(text: "true")] {{else}} [(text: "false")])]`},
{"simple range", "{{range .X}}hello{{end}}", noError,
`[({{range [(command: [F=[X]])]}} [(text: "hello")])]`},
{"chained field range", "{{range .X.Y.Z}}hello{{end}}", noError,
`[({{range [(command: [F=[X Y Z]])]}} [(text: "hello")])]`},
{"nested range", "{{range .X}}hello{{range .Y}}goodbye{{end}}{{end}}", noError,
`[({{range [(command: [F=[X]])]}} [(text: "hello")({{range [(command: [F=[Y]])]}} [(text: "goodbye")])])]`},
{"range with else", "{{range .X}}true{{else}}false{{end}}", noError,
`[({{range [(command: [F=[X]])]}} [(text: "true")] {{else}} [(text: "false")])]`},
{"range over pipeline", "{{range .X|.M}}true{{else}}false{{end}}", noError,
`[({{range [(command: [F=[X]]) (command: [F=[M]])]}} [(text: "true")] {{else}} [(text: "false")])]`},
{"range []int", "{{range .SI}}{{.}}{{end}}", noError,
`[({{range [(command: [F=[SI]])]}} [(action: [(command: [{{<.>}}])])])]`},
// Errors.
{"unclosed action", "hello{{range", hasError, ""},
{"not a field", "hello{{range x}}{{end}}", hasError, ""},
{"missing end", "hello{{range .x}}", hasError, ""},
{"missing end after else", "hello{{range .x}}{{else}}", hasError, ""},
}