gddo-server: consolidate template initialization

This is in preparation for passing around the template map explicitly
instead of using it as a global.  It also moves the manifest of template
sets into templates.go and out of main.go.

Change-Id: Ic7db8d5ab0186c1f4eb2446871e2c18ae1554d82
Reviewed-on: https://go-review.googlesource.com/67051
Reviewed-by: Tuo Shan <shantuo@google.com>
diff --git a/gddo-server/main.go b/gddo-server/main.go
index 9e94ef5..d970b94 100644
--- a/gddo-server/main.go
+++ b/gddo-server/main.go
@@ -310,10 +310,10 @@
 		}
 		template += templateExt(req)
 
-		return executeTemplate(resp, template, status, http.Header{"Etag": {etag}}, map[string]interface{}{
+		return templates.execute(resp, template, status, http.Header{"Etag": {etag}}, map[string]interface{}{
 			"flashMessages": flashMessages,
 			"pkgs":          pkgs,
-			"pdoc":          newTDoc(pdoc),
+			"pdoc":          newTDoc(viper.GetViper(), pdoc),
 			"importerCount": importerCount,
 		})
 	case isView(req, "imports"):
@@ -324,20 +324,20 @@
 		if err != nil {
 			return err
 		}
-		return executeTemplate(resp, "imports.html", http.StatusOK, nil, map[string]interface{}{
+		return templates.execute(resp, "imports.html", http.StatusOK, nil, map[string]interface{}{
 			"flashMessages": flashMessages,
 			"pkgs":          pkgs,
-			"pdoc":          newTDoc(pdoc),
+			"pdoc":          newTDoc(viper.GetViper(), pdoc),
 		})
 	case isView(req, "tools"):
 		proto := "http"
 		if req.Host == "godoc.org" {
 			proto = "https"
 		}
-		return executeTemplate(resp, "tools.html", http.StatusOK, nil, map[string]interface{}{
+		return templates.execute(resp, "tools.html", http.StatusOK, nil, map[string]interface{}{
 			"flashMessages": flashMessages,
 			"uri":           fmt.Sprintf("%s://%s/%s", proto, req.Host, importPath),
-			"pdoc":          newTDoc(pdoc),
+			"pdoc":          newTDoc(viper.GetViper(), pdoc),
 		})
 	case isView(req, "importers"):
 		if pdoc.Name == "" {
@@ -352,10 +352,10 @@
 			// Hide back links from robots.
 			template = "importers_robot.html"
 		}
-		return executeTemplate(resp, template, http.StatusOK, nil, map[string]interface{}{
+		return templates.execute(resp, template, http.StatusOK, nil, map[string]interface{}{
 			"flashMessages": flashMessages,
 			"pkgs":          pkgs,
-			"pdoc":          newTDoc(pdoc),
+			"pdoc":          newTDoc(viper.GetViper(), pdoc),
 		})
 	case isView(req, "import-graph"):
 		if requestType == robotRequest {
@@ -379,10 +379,10 @@
 		if err != nil {
 			return err
 		}
-		return executeTemplate(resp, "graph.html", http.StatusOK, nil, map[string]interface{}{
+		return templates.execute(resp, "graph.html", http.StatusOK, nil, map[string]interface{}{
 			"flashMessages": flashMessages,
 			"svg":           template.HTML(b),
-			"pdoc":          newTDoc(pdoc),
+			"pdoc":          newTDoc(viper.GetViper(), pdoc),
 			"hide":          hide,
 		})
 	case isView(req, "play"):
@@ -447,7 +447,7 @@
 	if err != nil {
 		return err
 	}
-	return executeTemplate(resp, "std.html", http.StatusOK, nil, map[string]interface{}{
+	return templates.execute(resp, "std.html", http.StatusOK, nil, map[string]interface{}{
 		"pkgs": pkgs,
 	})
 }
@@ -457,7 +457,7 @@
 	if err != nil {
 		return err
 	}
-	return executeTemplate(resp, "subrepo.html", http.StatusOK, nil, map[string]interface{}{
+	return templates.execute(resp, "subrepo.html", http.StatusOK, nil, map[string]interface{}{
 		"pkgs": pkgs,
 	})
 }
@@ -540,7 +540,7 @@
 			return err
 		}
 
-		return executeTemplate(resp, "home"+templateExt(req), http.StatusOK, nil,
+		return templates.execute(resp, "home"+templateExt(req), http.StatusOK, nil,
 			map[string]interface{}{"Popular": pkgs})
 	}
 
@@ -573,17 +573,17 @@
 		gceLogger.LogEvent(resp, req, logPkgs)
 	}
 
-	return executeTemplate(resp, "results"+templateExt(req), http.StatusOK, nil,
+	return templates.execute(resp, "results"+templateExt(req), http.StatusOK, nil,
 		map[string]interface{}{"q": q, "pkgs": pkgs})
 }
 
 func serveAbout(resp http.ResponseWriter, req *http.Request) error {
-	return executeTemplate(resp, "about.html", http.StatusOK, nil,
+	return templates.execute(resp, "about.html", http.StatusOK, nil,
 		map[string]interface{}{"Host": req.Host})
 }
 
 func serveBot(resp http.ResponseWriter, req *http.Request) error {
-	return executeTemplate(resp, "bot.html", http.StatusOK, nil, nil)
+	return templates.execute(resp, "bot.html", http.StatusOK, nil, nil)
 }
 
 func serveHealthCheck(resp http.ResponseWriter, req *http.Request) {
@@ -757,7 +757,7 @@
 func handleError(resp http.ResponseWriter, req *http.Request, status int, err error) {
 	switch status {
 	case http.StatusNotFound:
-		executeTemplate(resp, "notfound"+templateExt(req), status, nil, map[string]interface{}{
+		templates.execute(resp, "notfound"+templateExt(req), status, nil, map[string]interface{}{
 			"flashMessages": getFlashMessages(resp, req),
 		})
 	default:
@@ -846,38 +846,12 @@
 	doc.SetDefaultGOOS(viper.GetString(ConfigDefaultGOOS))
 	httpClient = newHTTPClient(viper.GetViper())
 
-	if err := parseHTMLTemplates([][]string{
-		{"about.html", "common.html", "layout.html"},
-		{"bot.html", "common.html", "layout.html"},
-		{"cmd.html", "common.html", "layout.html"},
-		{"dir.html", "common.html", "layout.html"},
-		{"home.html", "common.html", "layout.html"},
-		{"importers.html", "common.html", "layout.html"},
-		{"importers_robot.html", "common.html", "layout.html"},
-		{"imports.html", "common.html", "layout.html"},
-		{"notfound.html", "common.html", "layout.html"},
-		{"pkg.html", "common.html", "layout.html"},
-		{"results.html", "common.html", "layout.html"},
-		{"tools.html", "common.html", "layout.html"},
-		{"std.html", "common.html", "layout.html"},
-		{"subrepo.html", "common.html", "layout.html"},
-		{"graph.html", "common.html"},
-	}); err != nil {
-		log.Fatal(err)
-	}
-
-	if err := parseTextTemplates([][]string{
-		{"cmd.txt", "common.txt"},
-		{"dir.txt", "common.txt"},
-		{"home.txt", "common.txt"},
-		{"notfound.txt", "common.txt"},
-		{"pkg.txt", "common.txt"},
-		{"results.txt", "common.txt"},
-	}); err != nil {
-		log.Fatal(err)
-	}
-
 	var err error
+	templates, err = parseTemplates(viper.GetString(ConfigAssetsDir), viper.GetViper())
+	if err != nil {
+		log.Fatal(err)
+	}
+
 	db, err = database.New(
 		viper.GetString(ConfigDBServer),
 		viper.GetDuration(ConfigDBIdleTimeout),
diff --git a/gddo-server/template.go b/gddo-server/template.go
index 3aefd5b..8679607 100644
--- a/gddo-server/template.go
+++ b/gddo-server/template.go
@@ -80,7 +80,8 @@
 
 type tdoc struct {
 	*doc.Package
-	allExamples []*texample
+	allExamples    []*texample
+	sourcegraphURL string
 }
 
 type texample struct {
@@ -91,8 +92,11 @@
 	obj     interface{}
 }
 
-func newTDoc(pdoc *doc.Package) *tdoc {
-	return &tdoc{Package: pdoc}
+func newTDoc(v *viper.Viper, pdoc *doc.Package) *tdoc {
+	return &tdoc{
+		Package:        pdoc,
+		sourcegraphURL: v.GetString(ConfigSourcegraphURL),
+	}
 }
 
 func (pdoc *tdoc) SourceLink(pos doc.Pos, text string, textOnlyOK bool) htemp.HTML {
@@ -110,7 +114,7 @@
 // UsesLink generates a link to uses of a symbol definition.
 // title is used as the tooltip. defParts are parts of the symbol definition name.
 func (pdoc *tdoc) UsesLink(title string, defParts ...string) htemp.HTML {
-	if viper.GetString(ConfigSourcegraphURL) == "" {
+	if pdoc.sourcegraphURL == "" {
 		return ""
 	}
 
@@ -146,7 +150,7 @@
 		"pkg":  {pdoc.ImportPath},
 		"def":  {def},
 	}
-	u := viper.GetString(ConfigSourcegraphURL) + "/-/godoc/refs?" + q.Encode()
+	u := pdoc.sourcegraphURL + "/-/godoc/refs?" + q.Encode()
 	return htemp.HTML(fmt.Sprintf(`<a class="uses" title="%s" href="%s">Uses</a>`, htemp.HTMLEscapeString(title), htemp.HTMLEscapeString(u)))
 }
 
@@ -485,7 +489,15 @@
 	".txt":  textMIMEType,
 }
 
-func executeTemplate(resp http.ResponseWriter, name string, status int, header http.Header, data interface{}) error {
+// TODO(light): pass templates explicitly, not as a global.
+
+var templates templateMap
+
+type templateMap map[string]interface {
+	Execute(io.Writer, interface{}) error
+}
+
+func (m templateMap) execute(resp http.ResponseWriter, name string, status int, header http.Header, data interface{}) error {
 	for k, v := range header {
 		resp.Header()[k] = v
 	}
@@ -494,7 +506,7 @@
 		mimeType = textMIMEType
 	}
 	resp.Header().Set("Content-Type", mimeType)
-	t := templates[name]
+	t := m[name]
 	if t == nil {
 		return fmt.Errorf("template %s not found", name)
 	}
@@ -505,10 +517,6 @@
 	return t.Execute(resp, data)
 }
 
-var templates = map[string]interface {
-	Execute(io.Writer, interface{}) error
-}{}
-
 func joinTemplateDir(base string, files []string) []string {
 	result := make([]string, len(files))
 	for i := range files {
@@ -517,53 +525,76 @@
 	return result
 }
 
-func parseHTMLTemplates(sets [][]string) error {
-	for _, set := range sets {
+func parseTemplates(dir string, v *viper.Viper) (templateMap, error) {
+	m := make(templateMap)
+	htmlSets := [][]string{
+		{"about.html", "common.html", "layout.html"},
+		{"bot.html", "common.html", "layout.html"},
+		{"cmd.html", "common.html", "layout.html"},
+		{"dir.html", "common.html", "layout.html"},
+		{"home.html", "common.html", "layout.html"},
+		{"importers.html", "common.html", "layout.html"},
+		{"importers_robot.html", "common.html", "layout.html"},
+		{"imports.html", "common.html", "layout.html"},
+		{"notfound.html", "common.html", "layout.html"},
+		{"pkg.html", "common.html", "layout.html"},
+		{"results.html", "common.html", "layout.html"},
+		{"tools.html", "common.html", "layout.html"},
+		{"std.html", "common.html", "layout.html"},
+		{"subrepo.html", "common.html", "layout.html"},
+		{"graph.html", "common.html"},
+	}
+	hfuncs := htemp.FuncMap{
+		"code":              codeFn,
+		"comment":           commentFn,
+		"equal":             reflect.DeepEqual,
+		"gaAccount":         func() string { return v.GetString(ConfigGAAccount) },
+		"host":              hostFn,
+		"htmlComment":       htmlCommentFn,
+		"importPath":        importPathFn,
+		"isInterface":       isInterfaceFn,
+		"isValidImportPath": gosrc.IsValidPath,
+		"map":               mapFn,
+		"noteTitle":         noteTitleFn,
+		"relativePath":      relativePathFn,
+		"sidebarEnabled":    func() bool { return v.GetBool(ConfigSidebar) },
+		"staticPath":        func(p string) string { return cacheBusters.AppendQueryParam(p, "v") },
+	}
+	for _, set := range htmlSets {
 		templateName := set[0]
-		t := htemp.New("")
-		t.Funcs(htemp.FuncMap{
-			"code":              codeFn,
-			"comment":           commentFn,
-			"equal":             reflect.DeepEqual,
-			"gaAccount":         func() string { return viper.GetString(ConfigGAAccount) },
-			"host":              hostFn,
-			"htmlComment":       htmlCommentFn,
-			"importPath":        importPathFn,
-			"isInterface":       isInterfaceFn,
-			"isValidImportPath": gosrc.IsValidPath,
-			"map":               mapFn,
-			"noteTitle":         noteTitleFn,
-			"relativePath":      relativePathFn,
-			"sidebarEnabled":    func() bool { return viper.GetBool(ConfigSidebar) },
-			"staticPath":        func(p string) string { return cacheBusters.AppendQueryParam(p, "v") },
-			"templateName":      func() string { return templateName },
+		t := htemp.New("").Funcs(hfuncs).Funcs(htemp.FuncMap{
+			"templateName": func() string { return templateName },
 		})
-		if _, err := t.ParseFiles(joinTemplateDir(viper.GetString(ConfigAssetsDir), set)...); err != nil {
-			return err
+		if _, err := t.ParseFiles(joinTemplateDir(dir, set)...); err != nil {
+			return nil, err
 		}
 		t = t.Lookup("ROOT")
 		if t == nil {
-			return fmt.Errorf("ROOT template not found in %v", set)
+			return nil, fmt.Errorf("ROOT template not found in %v", set)
 		}
-		templates[set[0]] = t
+		m[set[0]] = t
 	}
-	return nil
-}
-
-func parseTextTemplates(sets [][]string) error {
-	for _, set := range sets {
-		t := ttemp.New("")
-		t.Funcs(ttemp.FuncMap{
-			"comment": commentTextFn,
-		})
-		if _, err := t.ParseFiles(joinTemplateDir(viper.GetString(ConfigAssetsDir), set)...); err != nil {
-			return err
+	textSets := [][]string{
+		{"cmd.txt", "common.txt"},
+		{"dir.txt", "common.txt"},
+		{"home.txt", "common.txt"},
+		{"notfound.txt", "common.txt"},
+		{"pkg.txt", "common.txt"},
+		{"results.txt", "common.txt"},
+	}
+	tfuncs := ttemp.FuncMap{
+		"comment": commentTextFn,
+	}
+	for _, set := range textSets {
+		t := ttemp.New("").Funcs(tfuncs)
+		if _, err := t.ParseFiles(joinTemplateDir(dir, set)...); err != nil {
+			return nil, err
 		}
 		t = t.Lookup("ROOT")
 		if t == nil {
-			return fmt.Errorf("ROOT template not found in %v", set)
+			return nil, fmt.Errorf("ROOT template not found in %v", set)
 		}
-		templates[set[0]] = t
+		m[set[0]] = t
 	}
-	return nil
+	return m, nil
 }