html/template: don't indirect past a Stringer

While we're here, get rid of the old names for the escaping functions.

Fixes #3073.

R=golang-dev, dsymonds, rsc
CC=golang-dev
https://golang.org/cl/5685049
diff --git a/src/pkg/html/template/content.go b/src/pkg/html/template/content.go
index 4de7ccd..539664f 100644
--- a/src/pkg/html/template/content.go
+++ b/src/pkg/html/template/content.go
@@ -85,6 +85,22 @@
 	return v.Interface()
 }
 
+var (
+	errorType       = reflect.TypeOf((*error)(nil)).Elem()
+	fmtStringerType = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
+)
+
+// indirectToStringerOrError returns the value, after dereferencing as many times
+// as necessary to reach the base type (or nil) or an implementation of fmt.Stringer
+// or error,
+func indirectToStringerOrError(a interface{}) interface{} {
+	v := reflect.ValueOf(a)
+	for !v.Type().Implements(fmtStringerType) && !v.Type().Implements(errorType) && v.Kind() == reflect.Ptr && !v.IsNil() {
+		v = v.Elem()
+	}
+	return v.Interface()
+}
+
 // stringify converts its arguments to a string and the type of the content.
 // All pointers are dereferenced, as in the text/template package.
 func stringify(args ...interface{}) (string, contentType) {
@@ -107,7 +123,7 @@
 		}
 	}
 	for i, arg := range args {
-		args[i] = indirect(arg)
+		args[i] = indirectToStringerOrError(arg)
 	}
 	return fmt.Sprint(args...), contentTypePlain
 }
diff --git a/src/pkg/html/template/content_test.go b/src/pkg/html/template/content_test.go
index c96a521..3c32e5e 100644
--- a/src/pkg/html/template/content_test.go
+++ b/src/pkg/html/template/content_test.go
@@ -6,6 +6,7 @@
 
 import (
 	"bytes"
+	"fmt"
 	"strings"
 	"testing"
 )
@@ -219,3 +220,42 @@
 		}
 	}
 }
+
+// Test that we print using the String method. Was issue 3073.
+type stringer struct {
+	v int
+}
+
+func (s *stringer) String() string {
+	return fmt.Sprintf("string=%d", s.v)
+}
+
+type errorer struct {
+	v int
+}
+
+func (s *errorer) Error() string {
+	return fmt.Sprintf("error=%d", s.v)
+}
+
+func TestStringer(t *testing.T) {
+	s := &stringer{3}
+	b := new(bytes.Buffer)
+	tmpl := Must(New("x").Parse("{{.}}"))
+	if err := tmpl.Execute(b, s); err != nil {
+		t.Fatal(err)
+	}
+	var expect = "string=3"
+	if b.String() != expect {
+		t.Errorf("expected %q got %q", expect, b.String())
+	}
+	e := &errorer{7}
+	b.Reset()
+	if err := tmpl.Execute(b, e); err != nil {
+		t.Fatal(err)
+	}
+	expect = "error=7"
+	if b.String() != expect {
+		t.Errorf("expected %q got %q", expect, b.String())
+	}
+}
diff --git a/src/pkg/html/template/escape.go b/src/pkg/html/template/escape.go
index 8145987..02fa3ea 100644
--- a/src/pkg/html/template/escape.go
+++ b/src/pkg/html/template/escape.go
@@ -46,30 +46,30 @@
 
 // funcMap maps command names to functions that render their inputs safe.
 var funcMap = template.FuncMap{
-	"exp_template_html_attrescaper":     attrEscaper,
-	"exp_template_html_commentescaper":  commentEscaper,
-	"exp_template_html_cssescaper":      cssEscaper,
-	"exp_template_html_cssvaluefilter":  cssValueFilter,
-	"exp_template_html_htmlnamefilter":  htmlNameFilter,
-	"exp_template_html_htmlescaper":     htmlEscaper,
-	"exp_template_html_jsregexpescaper": jsRegexpEscaper,
-	"exp_template_html_jsstrescaper":    jsStrEscaper,
-	"exp_template_html_jsvalescaper":    jsValEscaper,
-	"exp_template_html_nospaceescaper":  htmlNospaceEscaper,
-	"exp_template_html_rcdataescaper":   rcdataEscaper,
-	"exp_template_html_urlescaper":      urlEscaper,
-	"exp_template_html_urlfilter":       urlFilter,
-	"exp_template_html_urlnormalizer":   urlNormalizer,
+	"html_template_attrescaper":     attrEscaper,
+	"html_template_commentescaper":  commentEscaper,
+	"html_template_cssescaper":      cssEscaper,
+	"html_template_cssvaluefilter":  cssValueFilter,
+	"html_template_htmlnamefilter":  htmlNameFilter,
+	"html_template_htmlescaper":     htmlEscaper,
+	"html_template_jsregexpescaper": jsRegexpEscaper,
+	"html_template_jsstrescaper":    jsStrEscaper,
+	"html_template_jsvalescaper":    jsValEscaper,
+	"html_template_nospaceescaper":  htmlNospaceEscaper,
+	"html_template_rcdataescaper":   rcdataEscaper,
+	"html_template_urlescaper":      urlEscaper,
+	"html_template_urlfilter":       urlFilter,
+	"html_template_urlnormalizer":   urlNormalizer,
 }
 
 // equivEscapers matches contextual escapers to equivalent template builtins.
 var equivEscapers = map[string]string{
-	"exp_template_html_attrescaper":    "html",
-	"exp_template_html_htmlescaper":    "html",
-	"exp_template_html_nospaceescaper": "html",
-	"exp_template_html_rcdataescaper":  "html",
-	"exp_template_html_urlescaper":     "urlquery",
-	"exp_template_html_urlnormalizer":  "urlquery",
+	"html_template_attrescaper":    "html",
+	"html_template_htmlescaper":    "html",
+	"html_template_nospaceescaper": "html",
+	"html_template_rcdataescaper":  "html",
+	"html_template_urlescaper":     "urlquery",
+	"html_template_urlnormalizer":  "urlquery",
 }
 
 // escaper collects type inferences about templates and changes needed to make
@@ -147,17 +147,17 @@
 	case stateURL, stateCSSDqStr, stateCSSSqStr, stateCSSDqURL, stateCSSSqURL, stateCSSURL:
 		switch c.urlPart {
 		case urlPartNone:
-			s = append(s, "exp_template_html_urlfilter")
+			s = append(s, "html_template_urlfilter")
 			fallthrough
 		case urlPartPreQuery:
 			switch c.state {
 			case stateCSSDqStr, stateCSSSqStr:
-				s = append(s, "exp_template_html_cssescaper")
+				s = append(s, "html_template_cssescaper")
 			default:
-				s = append(s, "exp_template_html_urlnormalizer")
+				s = append(s, "html_template_urlnormalizer")
 			}
 		case urlPartQueryOrFrag:
-			s = append(s, "exp_template_html_urlescaper")
+			s = append(s, "html_template_urlescaper")
 		case urlPartUnknown:
 			return context{
 				state: stateError,
@@ -167,27 +167,27 @@
 			panic(c.urlPart.String())
 		}
 	case stateJS:
-		s = append(s, "exp_template_html_jsvalescaper")
+		s = append(s, "html_template_jsvalescaper")
 		// A slash after a value starts a div operator.
 		c.jsCtx = jsCtxDivOp
 	case stateJSDqStr, stateJSSqStr:
-		s = append(s, "exp_template_html_jsstrescaper")
+		s = append(s, "html_template_jsstrescaper")
 	case stateJSRegexp:
-		s = append(s, "exp_template_html_jsregexpescaper")
+		s = append(s, "html_template_jsregexpescaper")
 	case stateCSS:
-		s = append(s, "exp_template_html_cssvaluefilter")
+		s = append(s, "html_template_cssvaluefilter")
 	case stateText:
-		s = append(s, "exp_template_html_htmlescaper")
+		s = append(s, "html_template_htmlescaper")
 	case stateRCDATA:
-		s = append(s, "exp_template_html_rcdataescaper")
+		s = append(s, "html_template_rcdataescaper")
 	case stateAttr:
 		// Handled below in delim check.
 	case stateAttrName, stateTag:
 		c.state = stateAttrName
-		s = append(s, "exp_template_html_htmlnamefilter")
+		s = append(s, "html_template_htmlnamefilter")
 	default:
 		if isComment(c.state) {
-			s = append(s, "exp_template_html_commentescaper")
+			s = append(s, "html_template_commentescaper")
 		} else {
 			panic("unexpected state " + c.state.String())
 		}
@@ -196,9 +196,9 @@
 	case delimNone:
 		// No extra-escaping needed for raw text content.
 	case delimSpaceOrTagEnd:
-		s = append(s, "exp_template_html_nospaceescaper")
+		s = append(s, "html_template_nospaceescaper")
 	default:
-		s = append(s, "exp_template_html_attrescaper")
+		s = append(s, "html_template_attrescaper")
 	}
 	e.editActionNode(n, s)
 	return c
@@ -260,22 +260,22 @@
 // redundantFuncs[a][b] implies that funcMap[b](funcMap[a](x)) == funcMap[a](x)
 // for all x.
 var redundantFuncs = map[string]map[string]bool{
-	"exp_template_html_commentescaper": {
-		"exp_template_html_attrescaper":    true,
-		"exp_template_html_nospaceescaper": true,
-		"exp_template_html_htmlescaper":    true,
+	"html_template_commentescaper": {
+		"html_template_attrescaper":    true,
+		"html_template_nospaceescaper": true,
+		"html_template_htmlescaper":    true,
 	},
-	"exp_template_html_cssescaper": {
-		"exp_template_html_attrescaper": true,
+	"html_template_cssescaper": {
+		"html_template_attrescaper": true,
 	},
-	"exp_template_html_jsregexpescaper": {
-		"exp_template_html_attrescaper": true,
+	"html_template_jsregexpescaper": {
+		"html_template_attrescaper": true,
 	},
-	"exp_template_html_jsstrescaper": {
-		"exp_template_html_attrescaper": true,
+	"html_template_jsstrescaper": {
+		"html_template_attrescaper": true,
 	},
-	"exp_template_html_urlescaper": {
-		"exp_template_html_urlnormalizer": true,
+	"html_template_urlescaper": {
+		"html_template_urlnormalizer": true,
 	},
 }