mirror of
https://github.com/golang/go
synced 2024-11-18 16:04:44 -07:00
ca3319fbd2
This stuff was deleted from cmd/godoc, and is moving into pkg godoc. R=golang-dev, adg CC=golang-dev https://golang.org/cl/11425043
610 lines
16 KiB
Go
610 lines
16 KiB
Go
// 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.
|
|
|
|
package godoc
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/build"
|
|
"go/doc"
|
|
"go/token"
|
|
htmlpkg "html"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
pathpkg "path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
|
|
"code.google.com/p/go.tools/godoc/util"
|
|
"code.google.com/p/go.tools/godoc/vfs"
|
|
"code.google.com/p/go.tools/godoc/vfs/httpfs"
|
|
)
|
|
|
|
// TODO(bradfitz,adg): these are moved from godoc.go globals.
|
|
// Clean this up.
|
|
var (
|
|
FileServer http.Handler // default file server
|
|
CmdHandler Server
|
|
PkgHandler Server
|
|
|
|
// file system information
|
|
FSTree util.RWValue // *Directory tree of packages, updated with each sync (but sync code is removed now)
|
|
FSModified util.RWValue // timestamp of last call to invalidateIndex
|
|
DocMetadata util.RWValue // mapping from paths to *Metadata
|
|
)
|
|
|
|
func InitHandlers(fs vfs.FileSystem) {
|
|
FileServer = http.FileServer(httpfs.New(fs))
|
|
CmdHandler = Server{"/cmd/", "/src/cmd"}
|
|
PkgHandler = Server{"/pkg/", "/src/pkg"}
|
|
}
|
|
|
|
// Server is a godoc server.
|
|
type Server struct {
|
|
pattern string // url pattern; e.g. "/pkg/"
|
|
fsRoot string // file system root to which the pattern is mapped
|
|
}
|
|
|
|
func (s *Server) FSRoot() string { return s.fsRoot }
|
|
|
|
func (s *Server) RegisterWithMux(mux *http.ServeMux) {
|
|
mux.Handle(s.pattern, s)
|
|
}
|
|
|
|
// getPageInfo returns the PageInfo for a package directory abspath. If the
|
|
// parameter genAST is set, an AST containing only the package exports is
|
|
// computed (PageInfo.PAst), otherwise package documentation (PageInfo.Doc)
|
|
// is extracted from the AST. If there is no corresponding package in the
|
|
// directory, PageInfo.PAst and PageInfo.PDoc are nil. If there are no sub-
|
|
// directories, PageInfo.Dirs is nil. If an error occurred, PageInfo.Err is
|
|
// set to the respective error but the error is not logged.
|
|
//
|
|
func (h *Server) GetPageInfo(abspath, relpath string, mode PageInfoMode) *PageInfo {
|
|
info := &PageInfo{Dirname: abspath}
|
|
|
|
// Restrict to the package files that would be used when building
|
|
// the package on this system. This makes sure that if there are
|
|
// separate implementations for, say, Windows vs Unix, we don't
|
|
// jumble them all together.
|
|
// Note: Uses current binary's GOOS/GOARCH.
|
|
// To use different pair, such as if we allowed the user to choose,
|
|
// set ctxt.GOOS and ctxt.GOARCH before calling ctxt.ImportDir.
|
|
ctxt := build.Default
|
|
ctxt.IsAbsPath = pathpkg.IsAbs
|
|
ctxt.ReadDir = fsReadDir
|
|
ctxt.OpenFile = fsOpenFile
|
|
pkginfo, err := ctxt.ImportDir(abspath, 0)
|
|
// continue if there are no Go source files; we still want the directory info
|
|
if _, nogo := err.(*build.NoGoError); err != nil && !nogo {
|
|
info.Err = err
|
|
return info
|
|
}
|
|
|
|
// collect package files
|
|
pkgname := pkginfo.Name
|
|
pkgfiles := append(pkginfo.GoFiles, pkginfo.CgoFiles...)
|
|
if len(pkgfiles) == 0 {
|
|
// Commands written in C have no .go files in the build.
|
|
// Instead, documentation may be found in an ignored file.
|
|
// The file may be ignored via an explicit +build ignore
|
|
// constraint (recommended), or by defining the package
|
|
// documentation (historic).
|
|
pkgname = "main" // assume package main since pkginfo.Name == ""
|
|
pkgfiles = pkginfo.IgnoredGoFiles
|
|
}
|
|
|
|
// get package information, if any
|
|
if len(pkgfiles) > 0 {
|
|
// build package AST
|
|
fset := token.NewFileSet()
|
|
files, err := parseFiles(fset, abspath, pkgfiles)
|
|
if err != nil {
|
|
info.Err = err
|
|
return info
|
|
}
|
|
|
|
// ignore any errors - they are due to unresolved identifiers
|
|
pkg, _ := ast.NewPackage(fset, files, poorMansImporter, nil)
|
|
|
|
// extract package documentation
|
|
info.FSet = fset
|
|
if mode&ShowSource == 0 {
|
|
// show extracted documentation
|
|
var m doc.Mode
|
|
if mode&NoFiltering != 0 {
|
|
m = doc.AllDecls
|
|
}
|
|
if mode&AllMethods != 0 {
|
|
m |= doc.AllMethods
|
|
}
|
|
info.PDoc = doc.New(pkg, pathpkg.Clean(relpath), m) // no trailing '/' in importpath
|
|
|
|
// collect examples
|
|
testfiles := append(pkginfo.TestGoFiles, pkginfo.XTestGoFiles...)
|
|
files, err = parseFiles(fset, abspath, testfiles)
|
|
if err != nil {
|
|
log.Println("parsing examples:", err)
|
|
}
|
|
info.Examples = collectExamples(pkg, files)
|
|
|
|
// collect any notes that we want to show
|
|
if info.PDoc.Notes != nil {
|
|
// could regexp.Compile only once per godoc, but probably not worth it
|
|
if rx, err := regexp.Compile(NotesRx); err == nil {
|
|
for m, n := range info.PDoc.Notes {
|
|
if rx.MatchString(m) {
|
|
if info.Notes == nil {
|
|
info.Notes = make(map[string][]*doc.Note)
|
|
}
|
|
info.Notes[m] = n
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
} else {
|
|
// show source code
|
|
// TODO(gri) Consider eliminating export filtering in this mode,
|
|
// or perhaps eliminating the mode altogether.
|
|
if mode&NoFiltering == 0 {
|
|
packageExports(fset, pkg)
|
|
}
|
|
info.PAst = ast.MergePackageFiles(pkg, 0)
|
|
}
|
|
info.IsMain = pkgname == "main"
|
|
}
|
|
|
|
// get directory information, if any
|
|
var dir *Directory
|
|
var timestamp time.Time
|
|
if tree, ts := FSTree.Get(); tree != nil && tree.(*Directory) != nil {
|
|
// directory tree is present; lookup respective directory
|
|
// (may still fail if the file system was updated and the
|
|
// new directory tree has not yet been computed)
|
|
dir = tree.(*Directory).lookup(abspath)
|
|
timestamp = ts
|
|
}
|
|
if dir == nil {
|
|
// no directory tree present (too early after startup or
|
|
// command-line mode); compute one level for this page
|
|
// note: cannot use path filter here because in general
|
|
// it doesn't contain the FSTree path
|
|
dir = NewDirectory(abspath, 1)
|
|
timestamp = time.Now()
|
|
}
|
|
info.Dirs = dir.listing(true)
|
|
info.DirTime = timestamp
|
|
info.DirFlat = mode&FlatDir != 0
|
|
|
|
return info
|
|
}
|
|
|
|
func (h *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
if redirect(w, r) {
|
|
return
|
|
}
|
|
|
|
relpath := pathpkg.Clean(r.URL.Path[len(h.pattern):])
|
|
abspath := pathpkg.Join(h.fsRoot, relpath)
|
|
mode := GetPageInfoMode(r)
|
|
if relpath == BuiltinPkgPath {
|
|
mode = NoFiltering
|
|
}
|
|
info := h.GetPageInfo(abspath, relpath, mode)
|
|
if info.Err != nil {
|
|
log.Print(info.Err)
|
|
ServeError(w, r, relpath, info.Err)
|
|
return
|
|
}
|
|
|
|
if mode&NoHTML != 0 {
|
|
ServeText(w, applyTemplate(PackageText, "packageText", info))
|
|
return
|
|
}
|
|
|
|
var tabtitle, title, subtitle string
|
|
switch {
|
|
case info.PAst != nil:
|
|
tabtitle = info.PAst.Name.Name
|
|
case info.PDoc != nil:
|
|
tabtitle = info.PDoc.Name
|
|
default:
|
|
tabtitle = info.Dirname
|
|
title = "Directory "
|
|
if ShowTimestamps {
|
|
subtitle = "Last update: " + info.DirTime.String()
|
|
}
|
|
}
|
|
if title == "" {
|
|
if info.IsMain {
|
|
// assume that the directory name is the command name
|
|
_, tabtitle = pathpkg.Split(relpath)
|
|
title = "Command "
|
|
} else {
|
|
title = "Package "
|
|
}
|
|
}
|
|
title += tabtitle
|
|
|
|
// special cases for top-level package/command directories
|
|
switch tabtitle {
|
|
case "/src/pkg":
|
|
tabtitle = "Packages"
|
|
case "/src/cmd":
|
|
tabtitle = "Commands"
|
|
}
|
|
|
|
ServePage(w, Page{
|
|
Title: title,
|
|
Tabtitle: tabtitle,
|
|
Subtitle: subtitle,
|
|
Body: applyTemplate(PackageHTML, "packageHTML", info),
|
|
})
|
|
}
|
|
|
|
type PageInfoMode uint
|
|
|
|
const (
|
|
NoFiltering PageInfoMode = 1 << iota // do not filter exports
|
|
AllMethods // show all embedded methods
|
|
ShowSource // show source code, do not extract documentation
|
|
NoHTML // show result in textual form, do not generate HTML
|
|
FlatDir // show directory in a flat (non-indented) manner
|
|
)
|
|
|
|
// modeNames defines names for each PageInfoMode flag.
|
|
var modeNames = map[string]PageInfoMode{
|
|
"all": NoFiltering,
|
|
"methods": AllMethods,
|
|
"src": ShowSource,
|
|
"text": NoHTML,
|
|
"flat": FlatDir,
|
|
}
|
|
|
|
// GetPageInfoMode computes the PageInfoMode flags by analyzing the request
|
|
// URL form value "m". It is value is a comma-separated list of mode names
|
|
// as defined by modeNames (e.g.: m=src,text).
|
|
func GetPageInfoMode(r *http.Request) PageInfoMode {
|
|
var mode PageInfoMode
|
|
for _, k := range strings.Split(r.FormValue("m"), ",") {
|
|
if m, found := modeNames[strings.TrimSpace(k)]; found {
|
|
mode |= m
|
|
}
|
|
}
|
|
return AdjustPageInfoMode(r, mode)
|
|
}
|
|
|
|
// AdjustPageInfoMode allows specialized versions of godoc to adjust
|
|
// PageInfoMode by overriding this variable.
|
|
var AdjustPageInfoMode = func(_ *http.Request, mode PageInfoMode) PageInfoMode {
|
|
return mode
|
|
}
|
|
|
|
// fsReadDir implements ReadDir for the go/build package.
|
|
func fsReadDir(dir string) ([]os.FileInfo, error) {
|
|
return FS.ReadDir(filepath.ToSlash(dir))
|
|
}
|
|
|
|
// fsOpenFile implements OpenFile for the go/build package.
|
|
func fsOpenFile(name string) (r io.ReadCloser, err error) {
|
|
data, err := vfs.ReadFile(FS, filepath.ToSlash(name))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ioutil.NopCloser(bytes.NewReader(data)), nil
|
|
}
|
|
|
|
// poorMansImporter returns a (dummy) package object named
|
|
// by the last path component of the provided package path
|
|
// (as is the convention for packages). This is sufficient
|
|
// to resolve package identifiers without doing an actual
|
|
// import. It never returns an error.
|
|
//
|
|
func poorMansImporter(imports map[string]*ast.Object, path string) (*ast.Object, error) {
|
|
pkg := imports[path]
|
|
if pkg == nil {
|
|
// note that strings.LastIndex returns -1 if there is no "/"
|
|
pkg = ast.NewObj(ast.Pkg, path[strings.LastIndex(path, "/")+1:])
|
|
pkg.Data = ast.NewScope(nil) // required by ast.NewPackage for dot-import
|
|
imports[path] = pkg
|
|
}
|
|
return pkg, nil
|
|
}
|
|
|
|
// globalNames returns a set of the names declared by all package-level
|
|
// declarations. Method names are returned in the form Receiver_Method.
|
|
func globalNames(pkg *ast.Package) map[string]bool {
|
|
names := make(map[string]bool)
|
|
for _, file := range pkg.Files {
|
|
for _, decl := range file.Decls {
|
|
addNames(names, decl)
|
|
}
|
|
}
|
|
return names
|
|
}
|
|
|
|
// collectExamples collects examples for pkg from testfiles.
|
|
func collectExamples(pkg *ast.Package, testfiles map[string]*ast.File) []*doc.Example {
|
|
var files []*ast.File
|
|
for _, f := range testfiles {
|
|
files = append(files, f)
|
|
}
|
|
|
|
var examples []*doc.Example
|
|
globals := globalNames(pkg)
|
|
for _, e := range doc.Examples(files...) {
|
|
name := stripExampleSuffix(e.Name)
|
|
if name == "" || globals[name] {
|
|
examples = append(examples, e)
|
|
} else {
|
|
log.Printf("skipping example 'Example%s' because '%s' is not a known function or type", e.Name, e.Name)
|
|
}
|
|
}
|
|
|
|
return examples
|
|
}
|
|
|
|
// addNames adds the names declared by decl to the names set.
|
|
// Method names are added in the form ReceiverTypeName_Method.
|
|
func addNames(names map[string]bool, decl ast.Decl) {
|
|
switch d := decl.(type) {
|
|
case *ast.FuncDecl:
|
|
name := d.Name.Name
|
|
if d.Recv != nil {
|
|
var typeName string
|
|
switch r := d.Recv.List[0].Type.(type) {
|
|
case *ast.StarExpr:
|
|
typeName = r.X.(*ast.Ident).Name
|
|
case *ast.Ident:
|
|
typeName = r.Name
|
|
}
|
|
name = typeName + "_" + name
|
|
}
|
|
names[name] = true
|
|
case *ast.GenDecl:
|
|
for _, spec := range d.Specs {
|
|
switch s := spec.(type) {
|
|
case *ast.TypeSpec:
|
|
names[s.Name.Name] = true
|
|
case *ast.ValueSpec:
|
|
for _, id := range s.Names {
|
|
names[id.Name] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// packageExports is a local implementation of ast.PackageExports
|
|
// which correctly updates each package file's comment list.
|
|
// (The ast.PackageExports signature is frozen, hence the local
|
|
// implementation).
|
|
//
|
|
func packageExports(fset *token.FileSet, pkg *ast.Package) {
|
|
for _, src := range pkg.Files {
|
|
cmap := ast.NewCommentMap(fset, src, src.Comments)
|
|
ast.FileExports(src)
|
|
src.Comments = cmap.Filter(src).Comments()
|
|
}
|
|
}
|
|
|
|
func applyTemplate(t *template.Template, name string, data interface{}) []byte {
|
|
var buf bytes.Buffer
|
|
if err := t.Execute(&buf, data); err != nil {
|
|
log.Printf("%s.Execute: %s", name, err)
|
|
}
|
|
return buf.Bytes()
|
|
}
|
|
|
|
func redirect(w http.ResponseWriter, r *http.Request) (redirected bool) {
|
|
canonical := pathpkg.Clean(r.URL.Path)
|
|
if !strings.HasSuffix(canonical, "/") {
|
|
canonical += "/"
|
|
}
|
|
if r.URL.Path != canonical {
|
|
url := *r.URL
|
|
url.Path = canonical
|
|
http.Redirect(w, r, url.String(), http.StatusMovedPermanently)
|
|
redirected = true
|
|
}
|
|
return
|
|
}
|
|
|
|
func redirectFile(w http.ResponseWriter, r *http.Request) (redirected bool) {
|
|
c := pathpkg.Clean(r.URL.Path)
|
|
c = strings.TrimRight(c, "/")
|
|
if r.URL.Path != c {
|
|
url := *r.URL
|
|
url.Path = c
|
|
http.Redirect(w, r, url.String(), http.StatusMovedPermanently)
|
|
redirected = true
|
|
}
|
|
return
|
|
}
|
|
|
|
func serveTextFile(w http.ResponseWriter, r *http.Request, abspath, relpath, title string) {
|
|
src, err := vfs.ReadFile(FS, abspath)
|
|
if err != nil {
|
|
log.Printf("ReadFile: %s", err)
|
|
ServeError(w, r, relpath, err)
|
|
return
|
|
}
|
|
|
|
if r.FormValue("m") == "text" {
|
|
ServeText(w, src)
|
|
return
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
buf.WriteString("<pre>")
|
|
FormatText(&buf, src, 1, pathpkg.Ext(abspath) == ".go", r.FormValue("h"), RangeSelection(r.FormValue("s")))
|
|
buf.WriteString("</pre>")
|
|
fmt.Fprintf(&buf, `<p><a href="/%s?m=text">View as plain text</a></p>`, htmlpkg.EscapeString(relpath))
|
|
|
|
ServePage(w, Page{
|
|
Title: title + " " + relpath,
|
|
Tabtitle: relpath,
|
|
Body: buf.Bytes(),
|
|
})
|
|
}
|
|
|
|
func serveDirectory(w http.ResponseWriter, r *http.Request, abspath, relpath string) {
|
|
if redirect(w, r) {
|
|
return
|
|
}
|
|
|
|
list, err := FS.ReadDir(abspath)
|
|
if err != nil {
|
|
ServeError(w, r, relpath, err)
|
|
return
|
|
}
|
|
|
|
ServePage(w, Page{
|
|
Title: "Directory " + relpath,
|
|
Tabtitle: relpath,
|
|
Body: applyTemplate(DirlistHTML, "dirlistHTML", list),
|
|
})
|
|
}
|
|
|
|
func ServeHTMLDoc(w http.ResponseWriter, r *http.Request, abspath, relpath string) {
|
|
// get HTML body contents
|
|
src, err := vfs.ReadFile(FS, abspath)
|
|
if err != nil {
|
|
log.Printf("ReadFile: %s", err)
|
|
ServeError(w, r, relpath, err)
|
|
return
|
|
}
|
|
|
|
// if it begins with "<!DOCTYPE " assume it is standalone
|
|
// html that doesn't need the template wrapping.
|
|
if bytes.HasPrefix(src, doctype) {
|
|
w.Write(src)
|
|
return
|
|
}
|
|
|
|
// if it begins with a JSON blob, read in the metadata.
|
|
meta, src, err := extractMetadata(src)
|
|
if err != nil {
|
|
log.Printf("decoding metadata %s: %v", relpath, err)
|
|
}
|
|
|
|
// evaluate as template if indicated
|
|
if meta.Template {
|
|
tmpl, err := template.New("main").Funcs(TemplateFuncs).Parse(string(src))
|
|
if err != nil {
|
|
log.Printf("parsing template %s: %v", relpath, err)
|
|
ServeError(w, r, relpath, err)
|
|
return
|
|
}
|
|
var buf bytes.Buffer
|
|
if err := tmpl.Execute(&buf, nil); err != nil {
|
|
log.Printf("executing template %s: %v", relpath, err)
|
|
ServeError(w, r, relpath, err)
|
|
return
|
|
}
|
|
src = buf.Bytes()
|
|
}
|
|
|
|
// if it's the language spec, add tags to EBNF productions
|
|
if strings.HasSuffix(abspath, "go_spec.html") {
|
|
var buf bytes.Buffer
|
|
Linkify(&buf, src)
|
|
src = buf.Bytes()
|
|
}
|
|
|
|
ServePage(w, Page{
|
|
Title: meta.Title,
|
|
Subtitle: meta.Subtitle,
|
|
Body: src,
|
|
})
|
|
}
|
|
|
|
func serveFile(w http.ResponseWriter, r *http.Request) {
|
|
relpath := r.URL.Path
|
|
|
|
// Check to see if we need to redirect or serve another file.
|
|
if m := MetadataFor(relpath); m != nil {
|
|
if m.Path != relpath {
|
|
// Redirect to canonical path.
|
|
http.Redirect(w, r, m.Path, http.StatusMovedPermanently)
|
|
return
|
|
}
|
|
// Serve from the actual filesystem path.
|
|
relpath = m.filePath
|
|
}
|
|
|
|
abspath := relpath
|
|
relpath = relpath[1:] // strip leading slash
|
|
|
|
switch pathpkg.Ext(relpath) {
|
|
case ".html":
|
|
if strings.HasSuffix(relpath, "/index.html") {
|
|
// We'll show index.html for the directory.
|
|
// Use the dir/ version as canonical instead of dir/index.html.
|
|
http.Redirect(w, r, r.URL.Path[0:len(r.URL.Path)-len("index.html")], http.StatusMovedPermanently)
|
|
return
|
|
}
|
|
ServeHTMLDoc(w, r, abspath, relpath)
|
|
return
|
|
|
|
case ".go":
|
|
serveTextFile(w, r, abspath, relpath, "Source file")
|
|
return
|
|
}
|
|
|
|
dir, err := FS.Lstat(abspath)
|
|
if err != nil {
|
|
log.Print(err)
|
|
ServeError(w, r, relpath, err)
|
|
return
|
|
}
|
|
|
|
if dir != nil && dir.IsDir() {
|
|
if redirect(w, r) {
|
|
return
|
|
}
|
|
if index := pathpkg.Join(abspath, "index.html"); util.IsTextFile(FS, index) {
|
|
ServeHTMLDoc(w, r, index, index)
|
|
return
|
|
}
|
|
serveDirectory(w, r, abspath, relpath)
|
|
return
|
|
}
|
|
|
|
if util.IsTextFile(FS, abspath) {
|
|
if redirectFile(w, r) {
|
|
return
|
|
}
|
|
serveTextFile(w, r, abspath, relpath, "Text file")
|
|
return
|
|
}
|
|
|
|
FileServer.ServeHTTP(w, r)
|
|
}
|
|
|
|
func serveSearchDesc(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/opensearchdescription+xml")
|
|
data := map[string]interface{}{
|
|
"BaseURL": fmt.Sprintf("http://%s", r.Host),
|
|
}
|
|
if err := SearchDescXML.Execute(w, &data); err != nil && err != http.ErrBodyNotAllowed {
|
|
// Only log if there's an error that's not about writing on HEAD requests.
|
|
// See Issues 5451 and 5454.
|
|
log.Printf("searchDescXML.Execute: %s", err)
|
|
}
|
|
}
|
|
|
|
func ServeText(w http.ResponseWriter, text []byte) {
|
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
w.Write(text)
|
|
}
|