1
0
mirror of https://github.com/golang/go synced 2024-11-19 16:34:49 -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 TARG=template
GOFILES=\ GOFILES=\
exec.go\
lex.go\ lex.go\
parse.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 itemEOF
itemElse // else keyword itemElse // else keyword
itemEnd // end keyword itemEnd // end keyword
itemField // alphanumeric identifier, starting with '.'. itemField // alphanumeric identifier, starting with '.', possibly chained ('.x.y')
itemIdentifier // alphanumeric identifier itemIdentifier // alphanumeric identifier
itemIf // if keyword itemIf // if keyword
itemLeftMeta // left meta-string itemLeftMeta // left meta-string
@ -273,7 +273,9 @@ Loop:
for { for {
switch r := l.next(); { switch r := l.next(); {
case isAlphaNumeric(r): case isAlphaNumeric(r):
// absorb // absorb.
case r == '.' && l.input[l.start] == '.':
// field chaining; absorb into one token.
default: default:
l.backup() l.backup()
word := l.input[l.start:l.pos] word := l.input[l.start:l.pos]

View File

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

View File

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

View File

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