diff options
author | Ian Lance Taylor <iant@google.com> | 2016-02-03 21:58:02 +0000 |
---|---|---|
committer | Ian Lance Taylor <ian@gcc.gnu.org> | 2016-02-03 21:58:02 +0000 |
commit | f98dd1a338867a408f7c72d73fbad7fe7fc93e3a (patch) | |
tree | 2f8da9862a9c1fe0df138917f997b03439c02773 /libgo/go/text | |
parent | b081ed4efc144da0c45a6484aebfd10e0eb9fda3 (diff) | |
download | gcc-f98dd1a338867a408f7c72d73fbad7fe7fc93e3a.zip gcc-f98dd1a338867a408f7c72d73fbad7fe7fc93e3a.tar.gz gcc-f98dd1a338867a408f7c72d73fbad7fe7fc93e3a.tar.bz2 |
libgo: Update to go1.6rc1.
Reviewed-on: https://go-review.googlesource.com/19200
From-SVN: r233110
Diffstat (limited to 'libgo/go/text')
-rw-r--r-- | libgo/go/text/scanner/scanner.go | 2 | ||||
-rw-r--r-- | libgo/go/text/template/doc.go | 39 | ||||
-rw-r--r-- | libgo/go/text/template/exec.go | 69 | ||||
-rw-r--r-- | libgo/go/text/template/exec_test.go | 211 | ||||
-rw-r--r-- | libgo/go/text/template/funcs.go | 79 | ||||
-rw-r--r-- | libgo/go/text/template/multi_test.go | 22 | ||||
-rw-r--r-- | libgo/go/text/template/parse/lex.go | 98 | ||||
-rw-r--r-- | libgo/go/text/template/parse/lex_test.go | 31 | ||||
-rw-r--r-- | libgo/go/text/template/parse/parse.go | 83 | ||||
-rw-r--r-- | libgo/go/text/template/parse/parse_test.go | 34 | ||||
-rw-r--r-- | libgo/go/text/template/template.go | 37 |
11 files changed, 553 insertions, 152 deletions
diff --git a/libgo/go/text/scanner/scanner.go b/libgo/go/text/scanner/scanner.go index 3ab01ed..0155800 100644 --- a/libgo/go/text/scanner/scanner.go +++ b/libgo/go/text/scanner/scanner.go @@ -73,7 +73,7 @@ const ( GoTokens = ScanIdents | ScanFloats | ScanChars | ScanStrings | ScanRawStrings | ScanComments | SkipComments ) -// The result of Scan is one of the following tokens or a Unicode character. +// The result of Scan is one of these tokens or a Unicode character. const ( EOF = -(iota + 1) Ident diff --git a/libgo/go/text/template/doc.go b/libgo/go/text/template/doc.go index 0ce63f6..df8c95f 100644 --- a/libgo/go/text/template/doc.go +++ b/libgo/go/text/template/doc.go @@ -36,10 +36,35 @@ Here is a trivial example that prints "17 items are made of wool". More intricate examples appear below. +Text and spaces + +By default, all text between actions is copied verbatim when the template is +executed. For example, the string " items are made of " in the example above appears +on standard output when the program is run. + +However, to aid in formatting template source code, if an action's left delimiter +(by default "{{") is followed immediately by a minus sign and ASCII space character +("{{- "), all trailing white space is trimmed from the immediately preceding text. +Similarly, if the right delimiter ("}}") is preceded by a space and minus sign +(" -}}"), all leading white space is trimmed from the immediately following text. +In these trim markers, the ASCII space must be present; "{{-3}}" parses as an +action containing the number -3. + +For instance, when executing the template whose source is + + "{{23 -}} < {{- 45}}" + +the generated output would be + + "23<45" + +For this trimming, the definition of white space characters is the same as in Go: +space, horizontal tab, carriage return, and newline. + Actions Here is the list of actions. "Arguments" and "pipelines" are evaluations of -data, defined in detail below. +data, defined in detail in the corresponding sections that follow. */ // {{/* a comment */}} @@ -90,6 +115,14 @@ data, defined in detail below. The template with the specified name is executed with dot set to the value of the pipeline. + {{block "name" pipeline}} T1 {{end}} + A block is shorthand for defining a template + {{define "name"}} T1 {{end}} + and then executing it in place + {{template "name" .}} + The typical use is to define a set of root templates that are + then customized by redefining the block templates within. + {{with pipeline}} T1 {{end}} If the value of the pipeline is empty, no output is generated; otherwise, dot is set to the value of the pipeline and T1 is @@ -167,6 +200,8 @@ field of a struct, the function is not invoked automatically, but it can be used as a truth value for an if action and the like. To invoke it, use the call function, defined below. +Pipelines + A pipeline is a possibly chained sequence of "commands". A command is a simple value (argument) or a function or method call, possibly with multiple arguments: @@ -184,8 +219,6 @@ value (argument) or a function or method call, possibly with multiple arguments: function(Argument1, etc.) Functions and function names are described below. -Pipelines - A pipeline may be "chained" by separating a sequence of commands with pipeline characters '|'. In a chained pipeline, the result of the each command is passed as the last argument of the following command. The output of the final diff --git a/libgo/go/text/template/exec.go b/libgo/go/text/template/exec.go index daba788..efe1817 100644 --- a/libgo/go/text/template/exec.go +++ b/libgo/go/text/template/exec.go @@ -78,7 +78,23 @@ func doublePercent(str string) string { return str } -// errorf formats the error and terminates processing. +// TODO: It would be nice if ExecError was more broken down, but +// the way ErrorContext embeds the template name makes the +// processing too clumsy. + +// ExecError is the custom error type returned when Execute has an +// error evaluating its template. (If a write error occurs, the actual +// error is returned; it will not be of type ExecError.) +type ExecError struct { + Name string // Name of template. + Err error // Pre-formatted error. +} + +func (e ExecError) Error() string { + return e.Err.Error() +} + +// errorf records an ExecError and terminates processing. func (s *state) errorf(format string, args ...interface{}) { name := doublePercent(s.tmpl.Name()) if s.node == nil { @@ -87,7 +103,24 @@ func (s *state) errorf(format string, args ...interface{}) { location, context := s.tmpl.ErrorContext(s.node) format = fmt.Sprintf("template: %s: executing %q at <%s>: %s", location, name, doublePercent(context), format) } - panic(fmt.Errorf(format, args...)) + panic(ExecError{ + Name: s.tmpl.Name(), + Err: fmt.Errorf(format, args...), + }) +} + +// writeError is the wrapper type used internally when Execute has an +// error writing to its output. We strip the wrapper in errRecover. +// Note that this is not an implementation of error, so it cannot escape +// from the package as an error value. +type writeError struct { + Err error // Original error. +} + +func (s *state) writeError(err error) { + panic(writeError{ + Err: err, + }) } // errRecover is the handler that turns panics into returns from the top @@ -98,8 +131,10 @@ func errRecover(errp *error) { switch err := e.(type) { case runtime.Error: panic(e) - case error: - *errp = err + case writeError: + *errp = err.Err // Strip the wrapper. + case ExecError: + *errp = err // Keep the wrapper. default: panic(e) } @@ -145,7 +180,7 @@ func (t *Template) Execute(wr io.Writer, data interface{}) (err error) { } // DefinedTemplates returns a string listing the defined templates, -// prefixed by the string "defined templates are: ". If there are none, +// prefixed by the string "; defined templates are: ". If there are none, // it returns the empty string. For generating an error message here // and in html/template. func (t *Template) DefinedTemplates() string { @@ -193,7 +228,7 @@ func (s *state) walk(dot reflect.Value, node parse.Node) { s.walkTemplate(dot, node) case *parse.TextNode: if _, err := s.wr.Write(node.Text); err != nil { - s.errorf("%s", err) + s.writeError(err) } case *parse.WithNode: s.walkIfOrWith(parse.NodeWith, dot, node.Pipe, node.List, node.ElseList) @@ -222,8 +257,13 @@ func (s *state) walkIfOrWith(typ parse.NodeType, dot reflect.Value, pipe *parse. } } -// isTrue reports whether the value is 'true', in the sense of not the zero of its type, -// and whether the value has a meaningful truth value. +// IsTrue reports whether the value is 'true', in the sense of not the zero of its type, +// and whether the value has a meaningful truth value. This is the definition of +// truth used by if and other such actions. +func IsTrue(val interface{}) (truth, ok bool) { + return isTrue(reflect.ValueOf(val)) +} + func isTrue(val reflect.Value) (truth, ok bool) { if !val.IsValid() { // Something like var x interface{}, never set. It's a form of nil. @@ -483,7 +523,7 @@ func (s *state) evalField(dot reflect.Value, fieldName string, node parse.Node, return zero } typ := receiver.Type() - receiver, _ = indirect(receiver) + receiver, isNil := indirect(receiver) // Unless it's an interface, need to get to a value of type *T to guarantee // we see all methods of T and *T. ptr := receiver @@ -495,7 +535,6 @@ func (s *state) evalField(dot reflect.Value, fieldName string, node parse.Node, } hasArgs := len(args) > 1 || final.IsValid() // It's not a method; must be a field of a struct or an element of a map. The receiver must not be nil. - receiver, isNil := indirect(receiver) if isNil { s.errorf("nil pointer evaluating %s.%s", typ, fieldName) } @@ -789,16 +828,11 @@ func (s *state) evalEmptyInterface(dot reflect.Value, n parse.Node) reflect.Valu } // indirect returns the item at the end of indirection, and a bool to indicate if it's nil. -// We indirect through pointers and empty interfaces (only) because -// non-empty interfaces have methods we might need. func indirect(v reflect.Value) (rv reflect.Value, isNil bool) { for ; v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface; v = v.Elem() { if v.IsNil() { return v, true } - if v.Kind() == reflect.Interface && v.NumMethod() > 0 { - break - } } return v, false } @@ -811,7 +845,10 @@ func (s *state) printValue(n parse.Node, v reflect.Value) { if !ok { s.errorf("can't print %s of type %s", n, v.Type()) } - fmt.Fprint(s.wr, iface) + _, err := fmt.Fprint(s.wr, iface) + if err != nil { + s.writeError(err) + } } // printableValue returns the, possibly indirected, interface value inside v that diff --git a/libgo/go/text/template/exec_test.go b/libgo/go/text/template/exec_test.go index ba0e434..e507e91 100644 --- a/libgo/go/text/template/exec_test.go +++ b/libgo/go/text/template/exec_test.go @@ -9,6 +9,7 @@ import ( "errors" "flag" "fmt" + "io/ioutil" "reflect" "strings" "testing" @@ -50,8 +51,9 @@ type T struct { Empty2 interface{} Empty3 interface{} Empty4 interface{} - // Non-empty interface. - NonEmptyInterface I + // Non-empty interfaces. + NonEmptyInterface I + NonEmptyInterfacePtS *I // Stringer. Str fmt.Stringer Err error @@ -72,6 +74,12 @@ type T struct { unexported int } +type S []string + +func (S) Method0() string { + return "M0" +} + type U struct { V string } @@ -98,6 +106,8 @@ func (w *W) Error() string { return fmt.Sprintf("[%d]", w.k) } +var siVal = I(S{"a", "b"}) + var tVal = &T{ True: true, I: 17, @@ -118,22 +128,23 @@ var tVal = &T{ {"one": 1, "two": 2}, {"eleven": 11, "twelve": 12}, }, - Empty1: 3, - Empty2: "empty2", - Empty3: []int{7, 8}, - Empty4: &U{"UinEmpty"}, - NonEmptyInterface: new(T), - Str: bytes.NewBuffer([]byte("foozle")), - Err: errors.New("erroozle"), - PI: newInt(23), - PS: newString("a string"), - PSI: newIntSlice(21, 22, 23), - BinaryFunc: func(a, b string) string { return fmt.Sprintf("[%s=%s]", a, b) }, - VariadicFunc: func(s ...string) string { return fmt.Sprint("<", strings.Join(s, "+"), ">") }, - VariadicFuncInt: func(a int, s ...string) string { return fmt.Sprint(a, "=<", strings.Join(s, "+"), ">") }, - NilOKFunc: func(s *int) bool { return s == nil }, - ErrFunc: func() (string, error) { return "bla", nil }, - Tmpl: Must(New("x").Parse("test template")), // "x" is the value of .X + Empty1: 3, + Empty2: "empty2", + Empty3: []int{7, 8}, + Empty4: &U{"UinEmpty"}, + NonEmptyInterface: &T{X: "x"}, + NonEmptyInterfacePtS: &siVal, + Str: bytes.NewBuffer([]byte("foozle")), + Err: errors.New("erroozle"), + PI: newInt(23), + PS: newString("a string"), + PSI: newIntSlice(21, 22, 23), + BinaryFunc: func(a, b string) string { return fmt.Sprintf("[%s=%s]", a, b) }, + VariadicFunc: func(s ...string) string { return fmt.Sprint("<", strings.Join(s, "+"), ">") }, + VariadicFuncInt: func(a int, s ...string) string { return fmt.Sprint(a, "=<", strings.Join(s, "+"), ">") }, + NilOKFunc: func(s *int) bool { return s == nil }, + ErrFunc: func() (string, error) { return "bla", nil }, + Tmpl: Must(New("x").Parse("test template")), // "x" is the value of .X } // A non-empty interface. @@ -336,6 +347,7 @@ var execTests = []execTest{ {"if not .BinaryFunc call", "{{ if not .BinaryFunc}}{{call .BinaryFunc `1` `2`}}{{else}}No{{end}}", "No", tVal, true}, {"Interface Call", `{{stringer .S}}`, "foozle", map[string]interface{}{"S": bytes.NewBufferString("foozle")}, true}, {".ErrFunc", "{{call .ErrFunc}}", "bla", tVal, true}, + {"call nil", "{{call nil}}", "", tVal, false}, // Erroneous function calls (check args). {".BinaryFuncTooFew", "{{call .BinaryFunc `1`}}", "", tVal, false}, @@ -424,12 +436,15 @@ var execTests = []execTest{ {"slice[1]", "{{index .SI 1}}", "4", tVal, true}, {"slice[HUGE]", "{{index .SI 10}}", "", tVal, false}, {"slice[WRONG]", "{{index .SI `hello`}}", "", tVal, false}, + {"slice[nil]", "{{index .SI nil}}", "", tVal, false}, {"map[one]", "{{index .MSI `one`}}", "1", tVal, true}, {"map[two]", "{{index .MSI `two`}}", "2", tVal, true}, {"map[NO]", "{{index .MSI `XXX`}}", "0", tVal, true}, - {"map[nil]", "{{index .MSI nil}}", "0", tVal, true}, + {"map[nil]", "{{index .MSI nil}}", "", tVal, false}, + {"map[``]", "{{index .MSI ``}}", "0", tVal, true}, {"map[WRONG]", "{{index .MSI 10}}", "", tVal, false}, {"double index", "{{index .SMSI 1 `eleven`}}", "11", tVal, true}, + {"nil[1]", "{{index nil 1}}", "", tVal, false}, // Len. {"slice", "{{len .SI}}", "3", tVal, true}, @@ -545,6 +560,11 @@ var execTests = []execTest{ {"bug16i", "{{\"aaa\"|oneArg}}", "oneArg=aaa", tVal, true}, {"bug16j", "{{1+2i|printf \"%v\"}}", "(1+2i)", tVal, true}, {"bug16k", "{{\"aaa\"|printf }}", "aaa", tVal, true}, + {"bug17a", "{{.NonEmptyInterface.X}}", "x", tVal, true}, + {"bug17b", "-{{.NonEmptyInterface.Method1 1234}}-", "-1234-", tVal, true}, + {"bug17c", "{{len .NonEmptyInterfacePtS}}", "2", tVal, true}, + {"bug17d", "{{index .NonEmptyInterfacePtS 0}}", "a", tVal, true}, + {"bug17e", "{{range .NonEmptyInterfacePtS}}-{{.}}-{{end}}", "-a--b-", tVal, true}, } func zeroArgs() string { @@ -796,18 +816,19 @@ type Tree struct { } // Use different delimiters to test Set.Delims. +// Also test the trimming of leading and trailing spaces. const treeTemplate = ` - (define "tree") + (- define "tree" -) [ - (.Val) - (with .Left) - (template "tree" .) - (end) - (with .Right) - (template "tree" .) - (end) + (- .Val -) + (- with .Left -) + (template "tree" . -) + (- end -) + (- with .Right -) + (- template "tree" . -) + (- end -) ] - (end) + (- end -) ` func TestTree(t *testing.T) { @@ -852,19 +873,13 @@ func TestTree(t *testing.T) { t.Fatal("parse error:", err) } var b bytes.Buffer - stripSpace := func(r rune) rune { - if r == '\t' || r == '\n' { - return -1 - } - return r - } const expect = "[1[2[3[4]][5[6]]][7[8[9]][10[11]]]]" // First by looking up the template. err = tmpl.Lookup("tree").Execute(&b, tree) if err != nil { t.Fatal("exec error:", err) } - result := strings.Map(stripSpace, b.String()) + result := b.String() if result != expect { t.Errorf("expected %q got %q", expect, result) } @@ -874,7 +889,7 @@ func TestTree(t *testing.T) { if err != nil { t.Fatal("exec error:", err) } - result = strings.Map(stripSpace, b.String()) + result = b.String() if result != expect { t.Errorf("expected %q got %q", expect, result) } @@ -1141,3 +1156,127 @@ func TestUnterminatedStringError(t *testing.T) { t.Fatalf("unexpected error: %s", str) } } + +const alwaysErrorText = "always be failing" + +var alwaysError = errors.New(alwaysErrorText) + +type ErrorWriter int + +func (e ErrorWriter) Write(p []byte) (int, error) { + return 0, alwaysError +} + +func TestExecuteGivesExecError(t *testing.T) { + // First, a non-execution error shouldn't be an ExecError. + tmpl, err := New("X").Parse("hello") + if err != nil { + t.Fatal(err) + } + err = tmpl.Execute(ErrorWriter(0), 0) + if err == nil { + t.Fatal("expected error; got none") + } + if err.Error() != alwaysErrorText { + t.Errorf("expected %q error; got %q", alwaysErrorText, err) + } + // This one should be an ExecError. + tmpl, err = New("X").Parse("hello, {{.X.Y}}") + if err != nil { + t.Fatal(err) + } + err = tmpl.Execute(ioutil.Discard, 0) + if err == nil { + t.Fatal("expected error; got none") + } + eerr, ok := err.(ExecError) + if !ok { + t.Fatalf("did not expect ExecError %s", eerr) + } + expect := "field X in type int" + if !strings.Contains(err.Error(), expect) { + t.Errorf("expected %q; got %q", expect, err) + } +} + +func funcNameTestFunc() int { + return 0 +} + +func TestGoodFuncNames(t *testing.T) { + names := []string{ + "_", + "a", + "a1", + "a1", + "Ӵ", + } + for _, name := range names { + tmpl := New("X").Funcs( + FuncMap{ + name: funcNameTestFunc, + }, + ) + if tmpl == nil { + t.Fatalf("nil result for %q", name) + } + } +} + +func TestBadFuncNames(t *testing.T) { + names := []string{ + "", + "2", + "a-b", + } + for _, name := range names { + testBadFuncName(name, t) + } +} + +func testBadFuncName(name string, t *testing.T) { + defer func() { + recover() + }() + New("X").Funcs( + FuncMap{ + name: funcNameTestFunc, + }, + ) + // If we get here, the name did not cause a panic, which is how Funcs + // reports an error. + t.Errorf("%q succeeded incorrectly as function name", name) +} + +func TestBlock(t *testing.T) { + const ( + input = `a({{block "inner" .}}bar({{.}})baz{{end}})b` + want = `a(bar(hello)baz)b` + overlay = `{{define "inner"}}foo({{.}})bar{{end}}` + want2 = `a(foo(goodbye)bar)b` + ) + tmpl, err := New("outer").Parse(input) + if err != nil { + t.Fatal(err) + } + tmpl2, err := Must(tmpl.Clone()).Parse(overlay) + if err != nil { + t.Fatal(err) + } + + var buf bytes.Buffer + if err := tmpl.Execute(&buf, "hello"); err != nil { + t.Fatal(err) + } + if got := buf.String(); got != want { + t.Errorf("got %q, want %q", got, want) + } + + buf.Reset() + if err := tmpl2.Execute(&buf, "goodbye"); err != nil { + t.Fatal(err) + } + if got := buf.String(); got != want2 { + t.Errorf("got %q, want %q", got, want2) + } +} diff --git a/libgo/go/text/template/funcs.go b/libgo/go/text/template/funcs.go index ccd0dfc..49e9e74 100644 --- a/libgo/go/text/template/funcs.go +++ b/libgo/go/text/template/funcs.go @@ -58,6 +58,9 @@ func createValueFuncs(funcMap FuncMap) map[string]reflect.Value { // addValueFuncs adds to values the functions in funcs, converting them to reflect.Values. func addValueFuncs(out map[string]reflect.Value, in FuncMap) { for name, fn := range in { + if !goodName(name) { + panic(fmt.Errorf("function name %s is not a valid identifier", name)) + } v := reflect.ValueOf(fn) if v.Kind() != reflect.Func { panic("value for " + name + " not a function") @@ -77,7 +80,7 @@ func addFuncs(out, in FuncMap) { } } -// goodFunc checks that the function or method has the right result signature. +// goodFunc reports whether the function or method has the right result signature. func goodFunc(typ reflect.Type) bool { // We allow functions with 1 result or 2 results where the second is an error. switch { @@ -89,6 +92,23 @@ func goodFunc(typ reflect.Type) bool { return false } +// goodName reports whether the function name is a valid identifier. +func goodName(name string) bool { + if name == "" { + return false + } + for i, r := range name { + switch { + case r == '_': + case i == 0 && !unicode.IsLetter(r): + return false + case !unicode.IsLetter(r) && !unicode.IsDigit(r): + return false + } + } + return true +} + // findFunction looks for a function in the template, and global map. func findFunction(name string, tmpl *Template) (reflect.Value, bool) { if tmpl != nil && tmpl.common != nil { @@ -104,6 +124,21 @@ func findFunction(name string, tmpl *Template) (reflect.Value, bool) { return reflect.Value{}, false } +// prepareArg checks if value can be used as an argument of type argType, and +// converts an invalid value to appropriate zero if possible. +func prepareArg(value reflect.Value, argType reflect.Type) (reflect.Value, error) { + if !value.IsValid() { + if !canBeNil(argType) { + return reflect.Value{}, fmt.Errorf("value is nil; should be of type %s", argType) + } + value = reflect.Zero(argType) + } + if !value.Type().AssignableTo(argType) { + return reflect.Value{}, fmt.Errorf("value has type %s; should be %s", value.Type(), argType) + } + return value, nil +} + // Indexing. // index returns the result of indexing its first argument by the following @@ -111,6 +146,9 @@ func findFunction(name string, tmpl *Template) (reflect.Value, bool) { // indexed item must be a map, slice, or array. func index(item interface{}, indices ...interface{}) (interface{}, error) { v := reflect.ValueOf(item) + if !v.IsValid() { + return nil, fmt.Errorf("index of untyped nil") + } for _, i := range indices { index := reflect.ValueOf(i) var isNil bool @@ -125,6 +163,8 @@ func index(item interface{}, indices ...interface{}) (interface{}, error) { x = index.Int() case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: x = int64(index.Uint()) + case reflect.Invalid: + return nil, fmt.Errorf("cannot index slice/array with nil") default: return nil, fmt.Errorf("cannot index slice/array with type %s", index.Type()) } @@ -133,17 +173,18 @@ func index(item interface{}, indices ...interface{}) (interface{}, error) { } v = v.Index(int(x)) case reflect.Map: - if !index.IsValid() { - index = reflect.Zero(v.Type().Key()) - } - if !index.Type().AssignableTo(v.Type().Key()) { - return nil, fmt.Errorf("%s is not index type for %s", index.Type(), v.Type()) + index, err := prepareArg(index, v.Type().Key()) + if err != nil { + return nil, err } if x := v.MapIndex(index); x.IsValid() { v = x } else { v = reflect.Zero(v.Type().Elem()) } + case reflect.Invalid: + // the loop holds invariant: v.IsValid() + panic("unreachable") default: return nil, fmt.Errorf("can't index item of type %s", v.Type()) } @@ -155,7 +196,11 @@ func index(item interface{}, indices ...interface{}) (interface{}, error) { // length returns the length of the item, with an error if it has no defined length. func length(item interface{}) (int, error) { - v, isNil := indirect(reflect.ValueOf(item)) + v := reflect.ValueOf(item) + if !v.IsValid() { + return 0, fmt.Errorf("len of untyped nil") + } + v, isNil := indirect(v) if isNil { return 0, fmt.Errorf("len of nil pointer") } @@ -172,6 +217,9 @@ func length(item interface{}) (int, error) { // The function must return 1 result, or 2 results, the second of which is an error. func call(fn interface{}, args ...interface{}) (interface{}, error) { v := reflect.ValueOf(fn) + if !v.IsValid() { + return nil, fmt.Errorf("call of nil") + } typ := v.Type() if typ.Kind() != reflect.Func { return nil, fmt.Errorf("non-function of type %s", typ) @@ -201,13 +249,11 @@ func call(fn interface{}, args ...interface{}) (interface{}, error) { } else { argType = dddType } - if !value.IsValid() && canBeNil(argType) { - value = reflect.Zero(argType) - } - if !value.Type().AssignableTo(argType) { - return nil, fmt.Errorf("arg %d has type %s; should be %s", i, value.Type(), argType) + + var err error + if argv[i], err = prepareArg(value, argType); err != nil { + return nil, fmt.Errorf("arg %d: %s", i, err) } - argv[i] = value } result := v.Call(argv) if len(result) == 2 && !result[1].IsNil() { @@ -219,7 +265,7 @@ func call(fn interface{}, args ...interface{}) (interface{}, error) { // Boolean logic. func truth(a interface{}) bool { - t, _ := isTrue(reflect.ValueOf(a)) + t, _ := IsTrue(a) return t } @@ -254,9 +300,8 @@ func or(arg0 interface{}, args ...interface{}) interface{} { } // not returns the Boolean negation of its argument. -func not(arg interface{}) (truth bool) { - truth, _ = isTrue(reflect.ValueOf(arg)) - return !truth +func not(arg interface{}) bool { + return !truth(arg) } // Comparison. diff --git a/libgo/go/text/template/multi_test.go b/libgo/go/text/template/multi_test.go index ea01875..a8342f5 100644 --- a/libgo/go/text/template/multi_test.go +++ b/libgo/go/text/template/multi_test.go @@ -9,7 +9,6 @@ package template import ( "bytes" "fmt" - "strings" "testing" "text/template/parse" ) @@ -277,17 +276,11 @@ func TestRedefinition(t *testing.T) { if tmpl, err = New("tmpl1").Parse(`{{define "test"}}foo{{end}}`); err != nil { t.Fatalf("parse 1: %v", err) } - if _, err = tmpl.Parse(`{{define "test"}}bar{{end}}`); err == nil { - t.Fatal("expected error") + if _, err = tmpl.Parse(`{{define "test"}}bar{{end}}`); err != nil { + t.Fatalf("got error %v, expected nil", err) } - if !strings.Contains(err.Error(), "redefinition") { - t.Fatalf("expected redefinition error; got %v", err) - } - if _, err = tmpl.New("tmpl2").Parse(`{{define "test"}}bar{{end}}`); err == nil { - t.Fatal("expected error") - } - if !strings.Contains(err.Error(), "redefinition") { - t.Fatalf("expected redefinition error; got %v", err) + if _, err = tmpl.New("tmpl2").Parse(`{{define "test"}}bar{{end}}`); err != nil { + t.Fatalf("got error %v, expected nil", err) } } @@ -345,7 +338,6 @@ func TestNew(t *testing.T) { func TestParse(t *testing.T) { // In multiple calls to Parse with the same receiver template, only one call // can contain text other than space, comments, and template definitions - var err error t1 := New("test") if _, err := t1.Parse(`{{define "test"}}{{end}}`); err != nil { t.Fatalf("parsing test: %s", err) @@ -356,10 +348,4 @@ func TestParse(t *testing.T) { if _, err := t1.Parse(`{{define "test"}}foo{{end}}`); err != nil { t.Fatalf("parsing test: %s", err) } - if _, err = t1.Parse(`{{define "test"}}foo{{end}}`); err == nil { - t.Fatal("no error from redefining a template") - } - if !strings.Contains(err.Error(), "redefinition") { - t.Fatalf("expected redefinition error; got %v", err) - } } diff --git a/libgo/go/text/template/parse/lex.go b/libgo/go/text/template/parse/lex.go index 8f9fe1d..ea93e05 100644 --- a/libgo/go/text/template/parse/lex.go +++ b/libgo/go/text/template/parse/lex.go @@ -58,6 +58,7 @@ const ( itemVariable // variable starting with '$', such as '$' or '$1' or '$hello' // Keywords appear after all the rest. itemKeyword // used only to delimit the keywords + itemBlock // block keyword itemDot // the cursor, spelled '.' itemDefine // define keyword itemElse // else keyword @@ -71,6 +72,7 @@ const ( var key = map[string]itemType{ ".": itemDot, + "block": itemBlock, "define": itemDefine, "else": itemElse, "end": itemEnd, @@ -83,6 +85,21 @@ var key = map[string]itemType{ const eof = -1 +// Trimming spaces. +// If the action begins "{{- " rather than "{{", then all space/tab/newlines +// preceding the action are trimmed; conversely if it ends " -}}" the +// leading spaces are trimmed. This is done entirely in the lexer; the +// parser never sees it happen. We require an ASCII space to be +// present to avoid ambiguity with things like "{{-3}}". It reads +// better with the space present anyway. For simplicity, only ASCII +// space does the job. +const ( + spaceChars = " \t\r\n" // These are the space characters defined by Go itself. + leftTrimMarker = "- " // Attached to left delimiter, trims trailing spaces from preceding text. + rightTrimMarker = " -" // Attached to right delimiter, trims leading spaces from following text. + trimMarkerLen = Pos(len(leftTrimMarker)) +) + // stateFn represents the state of the scanner as a function that returns the next state. type stateFn func(*lexer) stateFn @@ -220,10 +237,18 @@ const ( // lexText scans until an opening action delimiter, "{{". func lexText(l *lexer) stateFn { for { - if strings.HasPrefix(l.input[l.pos:], l.leftDelim) { + delim, trimSpace := l.atLeftDelim() + if delim { + trimLength := Pos(0) + if trimSpace { + trimLength = rightTrimLength(l.input[l.start:l.pos]) + } + l.pos -= trimLength if l.pos > l.start { l.emit(itemText) } + l.pos += trimLength + l.ignore() return lexLeftDelim } if l.next() == eof { @@ -238,13 +263,56 @@ func lexText(l *lexer) stateFn { return nil } -// lexLeftDelim scans the left delimiter, which is known to be present. +// atLeftDelim reports whether the lexer is at a left delimiter, possibly followed by a trim marker. +func (l *lexer) atLeftDelim() (delim, trimSpaces bool) { + if !strings.HasPrefix(l.input[l.pos:], l.leftDelim) { + return false, false + } + // The left delim might have the marker afterwards. + trimSpaces = strings.HasPrefix(l.input[l.pos+Pos(len(l.leftDelim)):], leftTrimMarker) + return true, trimSpaces +} + +// rightTrimLength returns the length of the spaces at the end of the string. +func rightTrimLength(s string) Pos { + return Pos(len(s) - len(strings.TrimRight(s, spaceChars))) +} + +// atRightDelim reports whether the lexer is at a right delimiter, possibly preceded by a trim marker. +func (l *lexer) atRightDelim() (delim, trimSpaces bool) { + if strings.HasPrefix(l.input[l.pos:], l.rightDelim) { + return true, false + } + // The right delim might have the marker before. + if strings.HasPrefix(l.input[l.pos:], rightTrimMarker) { + if strings.HasPrefix(l.input[l.pos+trimMarkerLen:], l.rightDelim) { + return true, true + } + } + return false, false +} + +// leftTrimLength returns the length of the spaces at the beginning of the string. +func leftTrimLength(s string) Pos { + return Pos(len(s) - len(strings.TrimLeft(s, spaceChars))) +} + +// lexLeftDelim scans the left delimiter, which is known to be present, possibly with a trim marker. func lexLeftDelim(l *lexer) stateFn { l.pos += Pos(len(l.leftDelim)) - if strings.HasPrefix(l.input[l.pos:], leftComment) { + trimSpace := strings.HasPrefix(l.input[l.pos:], leftTrimMarker) + afterMarker := Pos(0) + if trimSpace { + afterMarker = trimMarkerLen + } + if strings.HasPrefix(l.input[l.pos+afterMarker:], leftComment) { + l.pos += afterMarker + l.ignore() return lexComment } l.emit(itemLeftDelim) + l.pos += afterMarker + l.ignore() l.parenDepth = 0 return lexInsideAction } @@ -257,19 +325,34 @@ func lexComment(l *lexer) stateFn { return l.errorf("unclosed comment") } l.pos += Pos(i + len(rightComment)) - if !strings.HasPrefix(l.input[l.pos:], l.rightDelim) { + delim, trimSpace := l.atRightDelim() + if !delim { return l.errorf("comment ends before closing delimiter") - + } + if trimSpace { + l.pos += trimMarkerLen } l.pos += Pos(len(l.rightDelim)) + if trimSpace { + l.pos += leftTrimLength(l.input[l.pos:]) + } l.ignore() return lexText } -// lexRightDelim scans the right delimiter, which is known to be present. +// lexRightDelim scans the right delimiter, which is known to be present, possibly with a trim marker. func lexRightDelim(l *lexer) stateFn { + trimSpace := strings.HasPrefix(l.input[l.pos:], rightTrimMarker) + if trimSpace { + l.pos += trimMarkerLen + l.ignore() + } l.pos += Pos(len(l.rightDelim)) l.emit(itemRightDelim) + if trimSpace { + l.pos += leftTrimLength(l.input[l.pos:]) + l.ignore() + } return lexText } @@ -278,7 +361,8 @@ func lexInsideAction(l *lexer) stateFn { // Either number, quoted string, or identifier. // Spaces separate arguments; runs of spaces turn into itemSpace. // Pipe symbols separate and are emitted. - if strings.HasPrefix(l.input[l.pos:], l.rightDelim) { + delim, _ := l.atRightDelim() + if delim { if l.parenDepth == 0 { return lexRightDelim } diff --git a/libgo/go/text/template/parse/lex_test.go b/libgo/go/text/template/parse/lex_test.go index be551d8..e35ebf1 100644 --- a/libgo/go/text/template/parse/lex_test.go +++ b/libgo/go/text/template/parse/lex_test.go @@ -33,6 +33,7 @@ var itemName = map[itemType]string{ // keywords itemDot: ".", + itemBlock: "block", itemDefine: "define", itemElse: "else", itemIf: "if", @@ -58,6 +59,8 @@ type lexTest struct { } var ( + tDot = item{itemDot, 0, "."} + tBlock = item{itemBlock, 0, "block"} tEOF = item{itemEOF, 0, ""} tFor = item{itemIdentifier, 0, "for"} tLeft = item{itemLeftDelim, 0, "{{"} @@ -104,6 +107,9 @@ var lexTests = []lexTest{ }}, {"empty action", `{{}}`, []item{tLeft, tRight, tEOF}}, {"for", `{{for}}`, []item{tLeft, tFor, tRight, tEOF}}, + {"block", `{{block "foo" .}}`, []item{ + tLeft, tBlock, tSpace, {itemString, 0, `"foo"`}, tSpace, tDot, tRight, tEOF, + }}, {"quote", `{{"abc \n\t\" "}}`, []item{tLeft, tQuote, tRight, tEOF}}, {"raw quote", "{{" + raw + "}}", []item{tLeft, tRawQuote, tRight, tEOF}}, {"raw quote with newline", "{{" + rawNL + "}}", []item{tLeft, tRawQuoteNL, tRight, tEOF}}, @@ -155,7 +161,7 @@ var lexTests = []lexTest{ }}, {"dot", "{{.}}", []item{ tLeft, - {itemDot, 0, "."}, + tDot, tRight, tEOF, }}, @@ -169,7 +175,7 @@ var lexTests = []lexTest{ tLeft, {itemField, 0, ".x"}, tSpace, - {itemDot, 0, "."}, + tDot, tSpace, {itemNumber, 0, ".2"}, tSpace, @@ -278,6 +284,19 @@ var lexTests = []lexTest{ tRight, tEOF, }}, + {"trimming spaces before and after", "hello- {{- 3 -}} -world", []item{ + {itemText, 0, "hello-"}, + tLeft, + {itemNumber, 0, "3"}, + tRight, + {itemText, 0, "-world"}, + tEOF, + }}, + {"trimming spaces before and after comment", "hello- {{- /* hello */ -}} -world", []item{ + {itemText, 0, "hello-"}, + {itemText, 0, "-world"}, + tEOF, + }}, // errors {"badchar", "#{{\x01}}", []item{ {itemText, 0, "#"}, @@ -339,7 +358,7 @@ var lexTests = []lexTest{ {itemText, 0, "hello-"}, {itemError, 0, `unclosed comment`}, }}, - {"text with comment close separted from delim", "hello-{{/* */ }}-world", []item{ + {"text with comment close separated from delim", "hello-{{/* */ }}-world", []item{ {itemText, 0, "hello-"}, {itemError, 0, `comment ends before closing delimiter`}, }}, @@ -488,9 +507,9 @@ func TestShutdown(t *testing.T) { func (t *Tree) parseLexer(lex *lexer, text string) (tree *Tree, err error) { defer t.recover(&err) t.ParseName = t.Name - t.startParse(nil, lex) - t.parse(nil) - t.add(nil) + t.startParse(nil, lex, map[string]*Tree{}) + t.parse() + t.add() t.stopParse() return t, nil } diff --git a/libgo/go/text/template/parse/parse.go b/libgo/go/text/template/parse/parse.go index 88aacd1..dc56cf7 100644 --- a/libgo/go/text/template/parse/parse.go +++ b/libgo/go/text/template/parse/parse.go @@ -28,6 +28,7 @@ type Tree struct { token [3]item // three-token lookahead for parser. peekCount int vars []string // variables defined at the moment. + treeSet map[string]*Tree } // Copy returns a copy of the Tree. Any parsing state is discarded. @@ -205,11 +206,12 @@ func (t *Tree) recover(errp *error) { } // startParse initializes the parser, using the lexer. -func (t *Tree) startParse(funcs []map[string]interface{}, lex *lexer) { +func (t *Tree) startParse(funcs []map[string]interface{}, lex *lexer, treeSet map[string]*Tree) { t.Root = nil t.lex = lex t.vars = []string{"$"} t.funcs = funcs + t.treeSet = treeSet } // stopParse terminates parsing. @@ -217,6 +219,7 @@ func (t *Tree) stopParse() { t.lex = nil t.vars = nil t.funcs = nil + t.treeSet = nil } // Parse parses the template definition string to construct a representation of @@ -226,19 +229,19 @@ func (t *Tree) stopParse() { func (t *Tree) Parse(text, leftDelim, rightDelim string, treeSet map[string]*Tree, funcs ...map[string]interface{}) (tree *Tree, err error) { defer t.recover(&err) t.ParseName = t.Name - t.startParse(funcs, lex(t.Name, text, leftDelim, rightDelim)) + t.startParse(funcs, lex(t.Name, text, leftDelim, rightDelim), treeSet) t.text = text - t.parse(treeSet) - t.add(treeSet) + t.parse() + t.add() t.stopParse() return t, nil } -// add adds tree to the treeSet. -func (t *Tree) add(treeSet map[string]*Tree) { - tree := treeSet[t.Name] +// add adds tree to t.treeSet. +func (t *Tree) add() { + tree := t.treeSet[t.Name] if tree == nil || IsEmptyTree(tree.Root) { - treeSet[t.Name] = t + t.treeSet[t.Name] = t return } if !IsEmptyTree(t.Root) { @@ -274,7 +277,7 @@ func IsEmptyTree(n Node) bool { // parse is the top-level parser for a template, essentially the same // as itemList except it also parses {{define}} actions. // It runs to EOF. -func (t *Tree) parse(treeSet map[string]*Tree) (next Node) { +func (t *Tree) parse() (next Node) { t.Root = t.newList(t.peek().pos) for t.peek().typ != itemEOF { if t.peek().typ == itemLeftDelim { @@ -283,8 +286,8 @@ func (t *Tree) parse(treeSet map[string]*Tree) (next Node) { newT := New("definition") // name will be updated once we know it. newT.text = t.text newT.ParseName = t.ParseName - newT.startParse(t.funcs, t.lex) - newT.parseDefinition(treeSet) + newT.startParse(t.funcs, t.lex, t.treeSet) + newT.parseDefinition() continue } t.backup2(delim) @@ -300,9 +303,9 @@ func (t *Tree) parse(treeSet map[string]*Tree) (next Node) { } // parseDefinition parses a {{define}} ... {{end}} template definition and -// installs the definition in the treeSet map. The "define" keyword has already +// installs the definition in t.treeSet. The "define" keyword has already // been scanned. -func (t *Tree) parseDefinition(treeSet map[string]*Tree) { +func (t *Tree) parseDefinition() { const context = "define clause" name := t.expectOneOf(itemString, itemRawString, context) var err error @@ -316,7 +319,7 @@ func (t *Tree) parseDefinition(treeSet map[string]*Tree) { if end.Type() != nodeEnd { t.errorf("unexpected %s in %s", end, context) } - t.add(treeSet) + t.add() t.stopParse() } @@ -358,6 +361,8 @@ func (t *Tree) textOrAction() Node { // First word could be a keyword such as range. func (t *Tree) action() (n Node) { switch token := t.nextNonSpace(); token.typ { + case itemBlock: + return t.blockControl() case itemElse: return t.elseControl() case itemEnd: @@ -522,13 +527,51 @@ func (t *Tree) elseControl() Node { return t.newElse(t.expect(itemRightDelim, "else").pos, t.lex.lineNumber()) } +// Block: +// {{block stringValue pipeline}} +// Block keyword is past. +// The name must be something that can evaluate to a string. +// The pipeline is mandatory. +func (t *Tree) blockControl() Node { + const context = "block clause" + + token := t.nextNonSpace() + name := t.parseTemplateName(token, context) + pipe := t.pipeline(context) + + block := New(name) // name will be updated once we know it. + block.text = t.text + block.ParseName = t.ParseName + block.startParse(t.funcs, t.lex, t.treeSet) + var end Node + block.Root, end = block.itemList() + if end.Type() != nodeEnd { + t.errorf("unexpected %s in %s", end, context) + } + block.add() + block.stopParse() + + return t.newTemplate(token.pos, t.lex.lineNumber(), name, pipe) +} + // Template: // {{template stringValue pipeline}} // Template keyword is past. The name must be something that can evaluate // to a string. func (t *Tree) templateControl() Node { - var name string + const context = "template clause" token := t.nextNonSpace() + name := t.parseTemplateName(token, context) + var pipe *PipeNode + if t.nextNonSpace().typ != itemRightDelim { + t.backup() + // Do not pop variables; they persist until "end". + pipe = t.pipeline(context) + } + return t.newTemplate(token.pos, t.lex.lineNumber(), name, pipe) +} + +func (t *Tree) parseTemplateName(token item, context string) (name string) { switch token.typ { case itemString, itemRawString: s, err := strconv.Unquote(token.val) @@ -537,15 +580,9 @@ func (t *Tree) templateControl() Node { } name = s default: - t.unexpected(token, "template invocation") - } - var pipe *PipeNode - if t.nextNonSpace().typ != itemRightDelim { - t.backup() - // Do not pop variables; they persist until "end". - pipe = t.pipeline("template") + t.unexpected(token, context) } - return t.newTemplate(token.pos, t.lex.lineNumber(), name, pipe) + return } // command: diff --git a/libgo/go/text/template/parse/parse_test.go b/libgo/go/text/template/parse/parse_test.go index 200d50c..b4512d3 100644 --- a/libgo/go/text/template/parse/parse_test.go +++ b/libgo/go/text/template/parse/parse_test.go @@ -228,6 +228,15 @@ var parseTests = []parseTest{ `{{with .X}}"hello"{{end}}`}, {"with with else", "{{with .X}}hello{{else}}goodbye{{end}}", noError, `{{with .X}}"hello"{{else}}"goodbye"{{end}}`}, + // Trimming spaces. + {"trim left", "x \r\n\t{{- 3}}", noError, `"x"{{3}}`}, + {"trim right", "{{3 -}}\n\n\ty", noError, `{{3}}"y"`}, + {"trim left and right", "x \r\n\t{{- 3 -}}\n\n\ty", noError, `"x"{{3}}"y"`}, + {"comment trim left", "x \r\n\t{{- /* hi */}}", noError, `"x"`}, + {"comment trim right", "{{/* hi */ -}}\n\n\ty", noError, `"y"`}, + {"comment trim left and right", "x \r\n\t{{- /* */ -}}\n\n\ty", noError, `"x""y"`}, + {"block definition", `{{block "foo" .}}hello{{end}}`, noError, + `{{template "foo" .}}`}, // Errors. {"unclosed action", "hello{{range", hasError, ""}, {"unmatched end", "{{end}}", hasError, ""}, @@ -277,6 +286,8 @@ var parseTests = []parseTest{ {"wrong pipeline boolean", "{{.|true}}", hasError, ""}, {"wrong pipeline nil", "{{'c'|nil}}", hasError, ""}, {"empty pipeline", `{{printf "%d" ( ) }}`, hasError, ""}, + // Missing pipeline in block + {"block definition", `{{block "foo"}}hello{{end}}`, hasError, ""}, } var builtins = map[string]interface{}{ @@ -450,3 +461,26 @@ func TestErrors(t *testing.T) { } } } + +func TestBlock(t *testing.T) { + const ( + input = `a{{block "inner" .}}bar{{.}}baz{{end}}b` + outer = `a{{template "inner" .}}b` + inner = `bar{{.}}baz` + ) + treeSet := make(map[string]*Tree) + tmpl, err := New("outer").Parse(input, "", "", treeSet, nil) + if err != nil { + t.Fatal(err) + } + if g, w := tmpl.Root.String(), outer; g != w { + t.Errorf("outer template = %q, want %q", g, w) + } + inTmpl := treeSet["inner"] + if inTmpl == nil { + t.Fatal("block did not define template") + } + if g, w := inTmpl.Root.String(), inner; g != w { + t.Errorf("inner template = %q, want %q", g, w) + } +} diff --git a/libgo/go/text/template/template.go b/libgo/go/text/template/template.go index 3e80982..7a7f42a 100644 --- a/libgo/go/text/template/template.go +++ b/libgo/go/text/template/template.go @@ -5,7 +5,6 @@ package template import ( - "fmt" "reflect" "sync" "text/template/parse" @@ -117,11 +116,10 @@ func (t *Template) copy(c *common) *Template { // AddParseTree adds parse tree for template with given name and associates it with t. // If the template does not already exist, it will create a new one. -// It is an error to reuse a name except to overwrite an empty template. +// If the template does exist, it will be replaced. func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error) { t.init() // If the name is the name of this template, overwrite this template. - // The associate method checks it's not a redefinition. nt := t if name != t.name { nt = t.New(name) @@ -162,8 +160,9 @@ func (t *Template) Delims(left, right string) *Template { // Funcs adds the elements of the argument map to the template's function map. // It panics if a value in the map is not a function with appropriate return -// type. However, it is legal to overwrite elements of the map. The return -// value is the template, so calls can be chained. +// type or if the name cannot be used syntactically as a function in a template. +// It is legal to overwrite elements of the map. The return value is the template, +// so calls can be chained. func (t *Template) Funcs(funcMap FuncMap) *Template { t.init() t.muFuncs.Lock() @@ -184,11 +183,7 @@ func (t *Template) Lookup(name string) *Template { // Parse defines the template by parsing the text. Nested template definitions will be // associated with the top-level template t. Parse may be called multiple times -// to parse definitions of templates to associate with t. It is an error if a -// resulting template is non-empty (contains content other than template -// definitions) and would replace a non-empty template with the same name. -// (In multiple calls to Parse with the same receiver template, only one call -// can contain text other than space, comments, and template definitions.) +// to parse definitions of templates to associate with t. func (t *Template) Parse(text string) (*Template, error) { t.init() t.muFuncs.RLock() @@ -207,25 +202,17 @@ func (t *Template) Parse(text string) (*Template, error) { } // associate installs the new template into the group of templates associated -// with t. It is an error to reuse a name except to overwrite an empty -// template. The two are already known to share the common structure. -// The boolean return value reports wither to store this tree as t.Tree. +// with t. The two are already known to share the common structure. +// The boolean return value reports whether to store this tree as t.Tree. func (t *Template) associate(new *Template, tree *parse.Tree) (bool, error) { if new.common != t.common { panic("internal error: associate not common") } - name := new.name - if old := t.tmpl[name]; old != nil { - oldIsEmpty := parse.IsEmptyTree(old.Root) - newIsEmpty := parse.IsEmptyTree(tree.Root) - if newIsEmpty { - // Whether old is empty or not, new is empty; no reason to replace old. - return false, nil - } - if !oldIsEmpty { - return false, fmt.Errorf("template: redefinition of template %q", name) - } + if t.tmpl[new.name] != nil && parse.IsEmptyTree(tree.Root) { + // If a template by that name exists, + // don't replace it with an empty template. + return false, nil } - t.tmpl[name] = new + t.tmpl[new.name] = new return true, nil } |