]> Cypherpunks repositories - gostls13.git/commitdiff
Add the beginnings of the template execution code. Lots still to do,
authorRob Pike <r@golang.org>
Tue, 28 Jun 2011 13:04:08 +0000 (23:04 +1000)
committerRob Pike <r@golang.org>
Tue, 28 Jun 2011 13:04:08 +0000 (23:04 +1000)
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

src/pkg/exp/template/Makefile
src/pkg/exp/template/exec.go [new file with mode: 0644]
src/pkg/exp/template/exec_test.go [new file with mode: 0644]
src/pkg/exp/template/lex.go
src/pkg/exp/template/lex_test.go
src/pkg/exp/template/parse.go
src/pkg/exp/template/parse_test.go

index ab9832f6137ef738e6f2829745c9ad6d6049f00f..2128ab1a570e49d9276f746a81803bacd83feb77 100644 (file)
@@ -6,6 +6,7 @@ include ../../../Make.inc
 
 TARG=template
 GOFILES=\
+       exec.go\
        lex.go\
        parse.go\
 
diff --git a/src/pkg/exp/template/exec.go b/src/pkg/exp/template/exec.go
new file mode 100644 (file)
index 0000000..2fbe305
--- /dev/null
@@ -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())
+}
diff --git a/src/pkg/exp/template/exec_test.go b/src/pkg/exp/template/exec_test.go
new file mode 100644 (file)
index 0000000..10348da
--- /dev/null
@@ -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)
+       }
+}
index 826d3eb8892e123c3de44dd914734f1ec3308d2c..51baa6e71e9daeff48d361a20c6298501a1684c9 100644 (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]
index 184e833efe2bc85e6db52eb95a7df2badfb81c78..beca41baa55dd621e91c8e445b993f9f50499a95 100644 (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,
        }},
index 57ddb0084fa6a5f225a68b0f79f38607e4a188f7..cfe180631ecd499f118c235e9fdf7bd384dfe093 100644 (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
 }
index f89eaa6ce3cfbc8b08c80dec0aea51e52005bd3b..e194c5e5d445827cd8e5fb69400f53e5e3c7ae5d 100644 (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, ""},
 }