|
- // 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(`<a title="View Source" href="%s">%s</a>`,
- 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(`<a class="uses" title="%s" href="%s">Uses</a>`, 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(`<span class="text-muted">/</span>`)
- }
- link := j < len(pdoc.ImportPath) ||
- (templateName != "dir.html" && templateName != "cmd.html" && templateName != "pkg.html")
- if link {
- buf.WriteString(`<a href="`)
- buf.WriteString(formatPathFrag(pdoc.ImportPath[:j], ""))
- buf.WriteString(`">`)
- } else {
- buf.WriteString(`<span class="text-muted">`)
- }
- buf.WriteString(htemp.HTMLEscapeString(pdoc.ImportPath[i:j]))
- if link {
- buf.WriteString("</a>")
- } else {
- buf.WriteString("</span>")
- }
- 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(`<h3 id="([^"]+)">([^<]+)</h3>`)
- 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, `<h4 id="`...)
- out = append(out, src[m[2]:m[3]]...)
- out = append(out, `">`...)
- out = append(out, src[m[4]:m[5]]...)
- out = append(out, ` <a class="permalink" href="#`...)
- out = append(out, src[m[2]:m[3]]...)
- out = append(out, `">¶</a></h4>`...)
- return out
- })
- p = replaceAll(p, rfcPat, func(out, src []byte, m []int) []byte {
- out = append(out, `<a href="http://tools.ietf.org/html/rfc`...)
- out = append(out, src[m[2]:m[3]]...)
-
- // If available, add section fragment
- if m[4] != -1 {
- out = append(out, `#section-`...)
- out = append(out, src[m[6]:m[7]]...)
- }
-
- out = append(out, `">`...)
- out = append(out, src[m[0]:m[1]]...)
- out = append(out, `</a>`...)
- 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, `<a href="/`...)
- out = append(out, path...)
- out = append(out, `">`...)
- out = append(out, path...)
- out = append(out, `</a>`...)
- 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("<pre>")
- for _, a := range c.Annotations {
- htemp.HTMLEscape(&buf, src[last:a.Pos])
- switch a.Kind {
- case doc.PackageLinkAnnotation:
- buf.WriteString(`<a href="`)
- buf.WriteString(formatPathFrag(c.Paths[a.PathIndex], ""))
- buf.WriteString(`">`)
- htemp.HTMLEscape(&buf, src[a.Pos:a.End])
- buf.WriteString(`</a>`)
- 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(`<a href="`)
- buf.WriteString(formatPathFrag(p, string(n)))
- buf.WriteString(`">`)
- htemp.HTMLEscape(&buf, src[a.Pos:a.End])
- buf.WriteString(`</a>`)
- case doc.CommentAnnotation:
- buf.WriteString(`<span class="com">`)
- htemp.HTMLEscape(&buf, src[a.Pos:a.End])
- buf.WriteString(`</span>`)
- case doc.AnchorAnnotation:
- buf.WriteString(`<span id="`)
- if typ != nil {
- htemp.HTMLEscape(&buf, []byte(typ.Name))
- buf.WriteByte('.')
- }
- htemp.HTMLEscape(&buf, src[a.Pos:a.End])
- buf.WriteString(`">`)
- htemp.HTMLEscape(&buf, src[a.Pos:a.End])
- buf.WriteString(`</span>`)
- default:
- htemp.HTMLEscape(&buf, src[a.Pos:a.End])
- }
- last = int(a.End)
- }
- htemp.HTMLEscape(&buf, src[last:])
- buf.WriteString("</pre>")
- 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("<!-- " + s + " -->")
- }
-
- 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
- }
|