// Copyright 2013 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 or at // https://developers.google.com/open-source/licenses/bsd. package main import ( "bytes" "encoding/base64" "errors" "fmt" godoc "go/doc" htemp "html/template" "io" "net/http" "net/url" "path" "path/filepath" "reflect" "regexp" "sort" "strings" ttemp "text/template" "time" "github.com/spf13/viper" "github.com/golang/gddo/doc" "github.com/golang/gddo/gosrc" "github.com/golang/gddo/httputil" ) var cacheBusters httputil.CacheBusters type flashMessage struct { ID string Args []string } // getFlashMessages retrieves flash messages from the request and clears the flash cookie if needed. func getFlashMessages(resp http.ResponseWriter, req *http.Request) []flashMessage { c, err := req.Cookie("flash") if err == http.ErrNoCookie { return nil } http.SetCookie(resp, &http.Cookie{Name: "flash", Path: "/", MaxAge: -1, Expires: time.Now().Add(-100 * 24 * time.Hour)}) if err != nil { return nil } p, err := base64.URLEncoding.DecodeString(c.Value) if err != nil { return nil } var messages []flashMessage for _, s := range strings.Split(string(p), "\000") { idArgs := strings.Split(s, "\001") messages = append(messages, flashMessage{ID: idArgs[0], Args: idArgs[1:]}) } return messages } // setFlashMessages sets a cookie with the given flash messages. func setFlashMessages(resp http.ResponseWriter, messages []flashMessage) { var buf []byte for i, message := range messages { if i > 0 { buf = append(buf, '\000') } buf = append(buf, message.ID...) for _, arg := range message.Args { buf = append(buf, '\001') buf = append(buf, arg...) } } value := base64.URLEncoding.EncodeToString(buf) http.SetCookie(resp, &http.Cookie{Name: "flash", Value: value, Path: "/"}) } type tdoc struct { *doc.Package allExamples []*texample } type texample struct { ID string Label string Example *doc.Example Play bool obj interface{} } func newTDoc(pdoc *doc.Package) *tdoc { return &tdoc{Package: pdoc} } func (pdoc *tdoc) SourceLink(pos doc.Pos, text string, textOnlyOK bool) htemp.HTML { if pos.Line == 0 || pdoc.LineFmt == "" || pdoc.Files[pos.File].URL == "" { if textOnlyOK { return htemp.HTML(htemp.HTMLEscapeString(text)) } return "" } return htemp.HTML(fmt.Sprintf(`%s`, htemp.HTMLEscapeString(fmt.Sprintf(pdoc.LineFmt, pdoc.Files[pos.File].URL, pos.Line)), htemp.HTMLEscapeString(text))) } // 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) == "" { return "" } var def string switch len(defParts) { case 1: // Funcs and types have one def part. def = defParts[0] case 3: // Methods have three def parts, the original receiver name, actual receiver name and method name. orig, recv, methodName := defParts[0], defParts[1], defParts[2] if orig == "" { // TODO: Remove this fallback after 2016-08-05. It's only needed temporarily to backfill data. // Actual receiver is not needed, it's only used because original receiver value // was recently added to gddo/doc package and will be blank until next package rebuild. // // Use actual receiver as fallback. orig = recv } // Trim "*" from "*T" if it's a pointer receiver method. typeName := strings.TrimPrefix(orig, "*") def = typeName + "/" + methodName default: panic(fmt.Errorf("%v defParts, want 1 or 3", len(defParts))) } q := url.Values{ "repo": {pdoc.ProjectRoot}, "pkg": {pdoc.ImportPath}, "def": {def}, } u := viper.GetString(ConfigSourcegraphURL) + "/-/godoc/refs?" + q.Encode() return htemp.HTML(fmt.Sprintf(`Uses`, htemp.HTMLEscapeString(title), htemp.HTMLEscapeString(u))) } func (pdoc *tdoc) PageName() string { if pdoc.Name != "" && !pdoc.IsCmd { return pdoc.Name } _, name := path.Split(pdoc.ImportPath) return name } func (pdoc *tdoc) addExamples(obj interface{}, export, method string, examples []*doc.Example) { label := export id := export if method != "" { label += "." + method id += "-" + method } for _, e := range examples { te := &texample{ Label: label, ID: id, Example: e, obj: obj, // Only show play links for packages within the standard library. Play: e.Play != "" && gosrc.IsGoRepoPath(pdoc.ImportPath), } if e.Name != "" { te.Label += " (" + e.Name + ")" if method == "" { te.ID += "-" } te.ID += "-" + e.Name } pdoc.allExamples = append(pdoc.allExamples, te) } } type byExampleID []*texample func (e byExampleID) Len() int { return len(e) } func (e byExampleID) Less(i, j int) bool { return e[i].ID < e[j].ID } func (e byExampleID) Swap(i, j int) { e[i], e[j] = e[j], e[i] } func (pdoc *tdoc) AllExamples() []*texample { if pdoc.allExamples != nil { return pdoc.allExamples } pdoc.allExamples = make([]*texample, 0) pdoc.addExamples(pdoc, "package", "", pdoc.Examples) for _, f := range pdoc.Funcs { pdoc.addExamples(f, f.Name, "", f.Examples) } for _, t := range pdoc.Types { pdoc.addExamples(t, t.Name, "", t.Examples) for _, f := range t.Funcs { pdoc.addExamples(f, f.Name, "", f.Examples) } for _, m := range t.Methods { if len(m.Examples) > 0 { pdoc.addExamples(m, t.Name, m.Name, m.Examples) } } } sort.Sort(byExampleID(pdoc.allExamples)) return pdoc.allExamples } func (pdoc *tdoc) ObjExamples(obj interface{}) []*texample { var examples []*texample for _, e := range pdoc.allExamples { if e.obj == obj { examples = append(examples, e) } } return examples } func (pdoc *tdoc) Breadcrumbs(templateName string) htemp.HTML { if !strings.HasPrefix(pdoc.ImportPath, pdoc.ProjectRoot) { return "" } var buf bytes.Buffer i := 0 j := len(pdoc.ProjectRoot) if j == 0 { j = strings.IndexRune(pdoc.ImportPath, '/') if j < 0 { j = len(pdoc.ImportPath) } } for { if i != 0 { buf.WriteString(`/`) } link := j < len(pdoc.ImportPath) || (templateName != "dir.html" && templateName != "cmd.html" && templateName != "pkg.html") if link { buf.WriteString(``) } else { buf.WriteString(``) } buf.WriteString(htemp.HTMLEscapeString(pdoc.ImportPath[i:j])) if link { buf.WriteString("") } else { buf.WriteString("") } i = j + 1 if i >= len(pdoc.ImportPath) { break } j = strings.IndexRune(pdoc.ImportPath[i:], '/') if j < 0 { j = len(pdoc.ImportPath) } else { j += i } } return htemp.HTML(buf.String()) } func (pdoc *tdoc) StatusDescription() htemp.HTML { desc := "" switch pdoc.Package.Status { case gosrc.DeadEndFork: desc = "This is a dead-end fork (no commits since the fork)." case gosrc.QuickFork: desc = "This is a quick bug-fix fork (has fewer than three commits, and only during the week it was created)." case gosrc.Inactive: desc = "This is an inactive package (no imports and no commits in at least two years)." } return htemp.HTML(desc) } func formatPathFrag(path, fragment string) string { if len(path) > 0 && path[0] != '/' { path = "/" + path } u := url.URL{Path: path, Fragment: fragment} return u.String() } func hostFn(urlStr string) string { u, err := url.Parse(urlStr) if err != nil { return "" } return u.Host } func mapFn(kvs ...interface{}) (map[string]interface{}, error) { if len(kvs)%2 != 0 { return nil, errors.New("map requires even number of arguments") } m := make(map[string]interface{}) for i := 0; i < len(kvs); i += 2 { s, ok := kvs[i].(string) if !ok { return nil, errors.New("even args to map must be strings") } m[s] = kvs[i+1] } return m, nil } // relativePathFn formats an import path as HTML. func relativePathFn(path string, parentPath interface{}) string { if p, ok := parentPath.(string); ok && p != "" && strings.HasPrefix(path, p) { path = path[len(p)+1:] } return path } // importPathFn formats an import with zero width space characters to allow for breaks. func importPathFn(path string) htemp.HTML { path = htemp.HTMLEscapeString(path) if len(path) > 45 { // Allow long import paths to break following "/" path = strings.Replace(path, "/", "/​", -1) } return htemp.HTML(path) } var ( h3Pat = regexp.MustCompile(`

([^<]+)

`) rfcPat = regexp.MustCompile(`RFC\s+(\d{3,4})(,?\s+[Ss]ection\s+(\d+(\.\d+)*))?`) packagePat = regexp.MustCompile(`\s+package\s+([-a-z0-9]\S+)`) ) func replaceAll(src []byte, re *regexp.Regexp, replace func(out, src []byte, m []int) []byte) []byte { var out []byte for len(src) > 0 { m := re.FindSubmatchIndex(src) if m == nil { break } out = append(out, src[:m[0]]...) out = replace(out, src, m) src = src[m[1]:] } if out == nil { return src } return append(out, src...) } // commentFn formats a source code comment as HTML. func commentFn(v string) htemp.HTML { var buf bytes.Buffer godoc.ToHTML(&buf, v, nil) p := buf.Bytes() p = replaceAll(p, h3Pat, func(out, src []byte, m []int) []byte { out = append(out, `

`...) out = append(out, src[m[4]:m[5]]...) out = append(out, `

`...) return out }) p = replaceAll(p, rfcPat, func(out, src []byte, m []int) []byte { out = append(out, ``...) out = append(out, src[m[0]:m[1]]...) out = append(out, ``...) return out }) p = replaceAll(p, packagePat, func(out, src []byte, m []int) []byte { path := bytes.TrimRight(src[m[2]:m[3]], ".!?:") if !gosrc.IsValidPath(string(path)) { return append(out, src[m[0]:m[1]]...) } out = append(out, src[m[0]:m[2]]...) out = append(out, ``...) out = append(out, path...) out = append(out, ``...) out = append(out, src[m[2]+len(path):m[1]]...) return out }) return htemp.HTML(p) } // commentTextFn formats a source code comment as text. func commentTextFn(v string) string { const indent = " " var buf bytes.Buffer godoc.ToText(&buf, v, indent, "\t", 80-2*len(indent)) p := buf.Bytes() return string(p) } var period = []byte{'.'} func codeFn(c doc.Code, typ *doc.Type) htemp.HTML { var buf bytes.Buffer last := 0 src := []byte(c.Text) buf.WriteString("
")
	for _, a := range c.Annotations {
		htemp.HTMLEscape(&buf, src[last:a.Pos])
		switch a.Kind {
		case doc.PackageLinkAnnotation:
			buf.WriteString(``)
			htemp.HTMLEscape(&buf, src[a.Pos:a.End])
			buf.WriteString(``)
		case doc.LinkAnnotation, doc.BuiltinAnnotation:
			var p string
			if a.Kind == doc.BuiltinAnnotation {
				p = "builtin"
			} else if a.PathIndex >= 0 {
				p = c.Paths[a.PathIndex]
			}
			n := src[a.Pos:a.End]
			n = n[bytes.LastIndex(n, period)+1:]
			buf.WriteString(``)
			htemp.HTMLEscape(&buf, src[a.Pos:a.End])
			buf.WriteString(``)
		case doc.CommentAnnotation:
			buf.WriteString(``)
			htemp.HTMLEscape(&buf, src[a.Pos:a.End])
			buf.WriteString(``)
		case doc.AnchorAnnotation:
			buf.WriteString(``)
			htemp.HTMLEscape(&buf, src[a.Pos:a.End])
			buf.WriteString(``)
		default:
			htemp.HTMLEscape(&buf, src[a.Pos:a.End])
		}
		last = int(a.End)
	}
	htemp.HTMLEscape(&buf, src[last:])
	buf.WriteString("
") return htemp.HTML(buf.String()) } var isInterfacePat = regexp.MustCompile(`^type [^ ]+ interface`) func isInterfaceFn(t *doc.Type) bool { return isInterfacePat.MatchString(t.Decl.Text) } var gaAccount string func gaAccountFn() string { return gaAccount } func noteTitleFn(s string) string { return strings.Title(strings.ToLower(s)) } func htmlCommentFn(s string) htemp.HTML { return htemp.HTML("") } var mimeTypes = map[string]string{ ".html": htmlMIMEType, ".txt": textMIMEType, } func executeTemplate(resp http.ResponseWriter, name string, status int, header http.Header, data interface{}) error { for k, v := range header { resp.Header()[k] = v } mimeType, ok := mimeTypes[path.Ext(name)] if !ok { mimeType = textMIMEType } resp.Header().Set("Content-Type", mimeType) t := templates[name] if t == nil { return fmt.Errorf("template %s not found", name) } resp.WriteHeader(status) if status == http.StatusNotModified { return nil } 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 { result[i] = filepath.Join(base, "templates", files[i]) } return result } func parseHTMLTemplates(sets [][]string) error { for _, set := range sets { templateName := set[0] t := htemp.New("") t.Funcs(htemp.FuncMap{ "code": codeFn, "comment": commentFn, "equal": reflect.DeepEqual, "gaAccount": gaAccountFn, "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 }, }) if _, err := t.ParseFiles(joinTemplateDir(viper.GetString(ConfigAssetsDir), set)...); err != nil { return err } t = t.Lookup("ROOT") if t == nil { return fmt.Errorf("ROOT template not found in %v", set) } templates[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 } t = t.Lookup("ROOT") if t == nil { return fmt.Errorf("ROOT template not found in %v", set) } templates[set[0]] = t } return nil }