TARG=template
GOFILES=\
+ exec.go\
lex.go\
parse.go\
--- /dev/null
+// 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())
+}
--- /dev/null
+// 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)
+ }
+}
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
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]
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,
}},
"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
nodeText nodeType = iota
nodeAction
nodeCommand
+ nodeDot
nodeElse
nodeEnd
nodeField
// 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 {
// 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) {
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.
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 {
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.
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
}
// 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()
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:
// {{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:
// Else keyword is past.
func (t *Template) elseControl() node {
t.expect(itemRightMeta, "else")
- return newElse()
+ return newElse(t.lex.lineNumber())
}
// command:
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:
t.unexpected(token, "command")
}
}
+ if len(cmd.args) == 0 {
+ t.errorf("empty command")
+ }
return cmd, nil
}
`[(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,
"[(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, ""},
}