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:
parent
5c15f8710c
commit
81592c298b
@ -6,6 +6,7 @@ include ../../../Make.inc
|
|||||||
|
|
||||||
TARG=template
|
TARG=template
|
||||||
GOFILES=\
|
GOFILES=\
|
||||||
|
exec.go\
|
||||||
lex.go\
|
lex.go\
|
||||||
parse.go\
|
parse.go\
|
||||||
|
|
||||||
|
316
src/pkg/exp/template/exec.go
Normal file
316
src/pkg/exp/template/exec.go
Normal 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())
|
||||||
|
}
|
160
src/pkg/exp/template/exec_test.go
Normal file
160
src/pkg/exp/template/exec_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -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]
|
||||||
|
@ -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,
|
||||||
}},
|
}},
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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, ""},
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user