diff options
Diffstat (limited to 'src/cmd/godoc/godoc.go')
-rw-r--r-- | src/cmd/godoc/godoc.go | 376 |
1 files changed, 104 insertions, 272 deletions
diff --git a/src/cmd/godoc/godoc.go b/src/cmd/godoc/godoc.go index e5f7a73d4..26814d2fa 100644 --- a/src/cmd/godoc/godoc.go +++ b/src/cmd/godoc/godoc.go @@ -20,7 +20,7 @@ import ( "net/http" "net/url" "os" - "path" + pathpkg "path" "path/filepath" "regexp" "runtime" @@ -55,12 +55,9 @@ var ( // file system roots // TODO(gri) consider the invariant that goroot always end in '/' - goroot = flag.String("goroot", runtime.GOROOT(), "Go root directory") - testDir = flag.String("testdir", "", "Go root subdirectory - for testing only (faster startups)") - pkgPath = flag.String("path", "", "additional package directories (colon-separated)") - filter = flag.String("filter", "", "filter file containing permitted package directory paths") - filterMin = flag.Int("filter_minutes", 0, "filter file update interval in minutes; disabled if <= 0") - filterDelay delayTime // actual filter update interval in minutes; usually filterDelay == filterMin, but filterDelay may back off exponentially + goroot = flag.String("goroot", runtime.GOROOT(), "Go root directory") + testDir = flag.String("testdir", "", "Go root subdirectory - for testing only (faster startups)") + pkgPath = flag.String("path", "", "additional package directories (colon-separated)") // layout control tabwidth = flag.Int("tabwidth", 4, "tab width") @@ -70,44 +67,42 @@ var ( // search index indexEnabled = flag.Bool("index", false, "enable search index") indexFiles = flag.String("index_files", "", "glob pattern specifying index files;"+ - "if not empty, the index is read from these files in sorted order") + "if not empty, the index is read from these files in sorted order") maxResults = flag.Int("maxresults", 10000, "maximum number of full text search results shown") indexThrottle = flag.Float64("index_throttle", 0.75, "index throttle value; 0.0 = no time allocated, 1.0 = full throttle") - // file system mapping - fs FileSystem // the underlying file system for godoc - fsHttp http.FileSystem // the underlying file system for http - fsMap Mapping // user-defined mapping - fsTree RWValue // *Directory tree of packages, updated with each sync - pathFilter RWValue // filter used when building fsMap directory trees - fsModified RWValue // timestamp of last call to invalidateIndex - docMetadata RWValue // mapping from paths to *Metadata + // file system information + fsTree RWValue // *Directory tree of packages, updated with each sync (but sync code is removed now) + fsModified RWValue // timestamp of last call to invalidateIndex + docMetadata RWValue // mapping from paths to *Metadata // http handlers fileServer http.Handler // default file server - cmdHandler httpHandler - pkgHandler httpHandler + cmdHandler docServer + pkgHandler docServer ) func initHandlers() { - paths := filepath.SplitList(*pkgPath) - gorootSrc := filepath.Join(build.Default.GOROOT, "src", "pkg") - for _, p := range build.Default.SrcDirs() { - if p != gorootSrc { - paths = append(paths, p) + // Add named directories in -path argument as + // subdirectories of src/pkg. + for _, p := range filepath.SplitList(*pkgPath) { + _, elem := filepath.Split(p) + if elem == "" { + log.Fatalf("invalid -path argument: %q has no final element", p) } + fs.Bind("/src/pkg/"+elem, OS(p), "/", bindReplace) } - fsMap.Init(paths) - fileServer = http.FileServer(fsHttp) - cmdHandler = httpHandler{"/cmd/", filepath.Join(*goroot, "src", "cmd"), false} - pkgHandler = httpHandler{"/pkg/", filepath.Join(*goroot, "src", "pkg"), true} + fileServer = http.FileServer(&httpFS{fs}) + cmdHandler = docServer{"/cmd/", "/src/cmd", false} + pkgHandler = docServer{"/pkg/", "/src/pkg", true} } func registerPublicHandlers(mux *http.ServeMux) { mux.Handle(cmdHandler.pattern, &cmdHandler) mux.Handle(pkgHandler.pattern, &pkgHandler) mux.HandleFunc("/doc/codewalk/", codewalk) + mux.Handle("/doc/play/", fileServer) mux.HandleFunc("/search", search) mux.Handle("/robots.txt", fileServer) mux.HandleFunc("/opensearch.xml", serveSearchDesc) @@ -115,7 +110,7 @@ func registerPublicHandlers(mux *http.ServeMux) { } func initFSTree() { - dir := newDirectory(filepath.Join(*goroot, *testDir), nil, -1) + dir := newDirectory(pathpkg.Join("/", *testDir), -1) if dir == nil { log.Println("Warning: FSTree is nil") return @@ -125,177 +120,6 @@ func initFSTree() { } // ---------------------------------------------------------------------------- -// Directory filters - -// isParentOf returns true if p is a parent of (or the same as) q -// where p and q are directory paths. -func isParentOf(p, q string) bool { - n := len(p) - return strings.HasPrefix(q, p) && (len(q) <= n || q[n] == '/') -} - -func setPathFilter(list []string) { - if len(list) == 0 { - pathFilter.set(nil) - return - } - - // len(list) > 0 - pathFilter.set(func(path string) bool { - // list is sorted in increasing order and for each path all its children are removed - i := sort.Search(len(list), func(i int) bool { return list[i] > path }) - // Now we have list[i-1] <= path < list[i]. - // Path may be a child of list[i-1] or a parent of list[i]. - return i > 0 && isParentOf(list[i-1], path) || i < len(list) && isParentOf(path, list[i]) - }) -} - -func getPathFilter() func(string) bool { - f, _ := pathFilter.get() - if f != nil { - return f.(func(string) bool) - } - return nil -} - -// readDirList reads a file containing a newline-separated list -// of directory paths and returns the list of paths. -func readDirList(filename string) ([]string, error) { - contents, err := ReadFile(fs, filename) - if err != nil { - return nil, err - } - // create a sorted list of valid directory names - filter := func(path string) bool { - d, e := fs.Lstat(path) - if e != nil && err == nil { - // remember first error and return it from readDirList - // so we have at least some information if things go bad - err = e - } - return e == nil && isPkgDir(d) - } - list := canonicalizePaths(strings.Split(string(contents), "\n"), filter) - // for each parent path, remove all its children q - // (requirement for binary search to work when filtering) - i := 0 - for _, q := range list { - if i == 0 || !isParentOf(list[i-1], q) { - list[i] = q - i++ - } - } - return list[0:i], err -} - -// updateMappedDirs computes the directory tree for -// each user-defined file system mapping. If a filter -// is provided, it is used to filter directories. -// -func updateMappedDirs(filter func(string) bool) { - if !fsMap.IsEmpty() { - fsMap.Iterate(func(path string, value *RWValue) bool { - value.set(newDirectory(path, filter, -1)) - return true - }) - invalidateIndex() - } -} - -func updateFilterFile() { - updateMappedDirs(nil) // no filter for accuracy - - // collect directory tree leaf node paths - var buf bytes.Buffer - fsMap.Iterate(func(_ string, value *RWValue) bool { - v, _ := value.get() - if v != nil && v.(*Directory) != nil { - v.(*Directory).writeLeafs(&buf) - } - return true - }) - - // update filter file - if err := writeFileAtomically(*filter, buf.Bytes()); err != nil { - log.Printf("writeFileAtomically(%s): %s", *filter, err) - filterDelay.backoff(24 * time.Hour) // back off exponentially, but try at least once a day - } else { - filterDelay.set(*filterMin) // revert to regular filter update schedule - } -} - -func initDirTrees() { - // setup initial path filter - if *filter != "" { - list, err := readDirList(*filter) - if err != nil { - log.Printf("readDirList(%s): %s", *filter, err) - } - if *verbose || len(list) == 0 { - log.Printf("found %d directory paths in file %s", len(list), *filter) - } - setPathFilter(list) - } - - go updateMappedDirs(getPathFilter()) // use filter for speed - - // start filter update goroutine, if enabled. - if *filter != "" && *filterMin > 0 { - filterDelay.set(time.Duration(*filterMin) * time.Minute) // initial filter update delay - go func() { - for { - if *verbose { - log.Printf("start update of %s", *filter) - } - updateFilterFile() - delay, _ := filterDelay.get() - dt := delay.(time.Duration) - if *verbose { - log.Printf("next filter update in %s", dt) - } - time.Sleep(dt) - } - }() - } -} - -// ---------------------------------------------------------------------------- -// Path mapping - -// Absolute paths are file system paths (backslash-separated on Windows), -// but relative paths are always slash-separated. - -func absolutePath(relpath, defaultRoot string) string { - abspath := fsMap.ToAbsolute(relpath) - if abspath == "" { - // no user-defined mapping found; use default mapping - abspath = filepath.Join(defaultRoot, filepath.FromSlash(relpath)) - } - return abspath -} - -func relativeURL(abspath string) string { - relpath := fsMap.ToRelative(abspath) - if relpath == "" { - // prefix must end in a path separator - prefix := *goroot - if len(prefix) > 0 && prefix[len(prefix)-1] != filepath.Separator { - prefix += string(filepath.Separator) - } - if strings.HasPrefix(abspath, prefix) { - // no user-defined mapping found; use default mapping - relpath = filepath.ToSlash(abspath[len(prefix):]) - } - } - // Only if path is an invalid absolute path is relpath == "" - // at this point. This should never happen since absolute paths - // are only created via godoc for files that do exist. However, - // it is ok to return ""; it will simply provide a link to the - // top of the pkg or src directories. - return relpath -} - -// ---------------------------------------------------------------------------- // Tab conversion var spaces = []byte(" ") // 32 spaces seems like a good number @@ -391,7 +215,7 @@ func writeNode(w io.Writer, fset *token.FileSet, x interface{}) { } func filenameFunc(path string) string { - _, localname := filepath.Split(path) + _, localname := pathpkg.Split(path) return localname } @@ -581,7 +405,7 @@ func splitExampleName(s string) (name, suffix string) { } func pkgLinkFunc(path string) string { - relpath := relativeURL(path) + relpath := path[1:] // because of the irregular mapping under goroot // we need to correct certain relative paths if strings.HasPrefix(relpath, "src/pkg/") { @@ -597,7 +421,7 @@ func posLink_urlFunc(node ast.Node, fset *token.FileSet) string { if p := node.Pos(); p.IsValid() { pos := fset.Position(p) - relpath = relativeURL(pos.Filename) + relpath = pos.Filename line = pos.Line low = pos.Offset } @@ -626,6 +450,10 @@ func posLink_urlFunc(node ast.Node, fset *token.FileSet) string { return buf.String() } +func srcLinkFunc(s string) string { + return pathpkg.Clean("/" + s) +} + // fmap describes the template functions installed with all godoc templates. // Convention: template function names ending in "_html" or "_url" produce // HTML- or URL-escaped strings; all other function results may @@ -652,7 +480,7 @@ var fmap = template.FuncMap{ // support for URL attributes "pkgLink": pkgLinkFunc, - "srcLink": relativeURL, + "srcLink": srcLinkFunc, "posLink_url": posLink_urlFunc, // formatting of Examples @@ -662,10 +490,10 @@ var fmap = template.FuncMap{ } func readTemplate(name string) *template.Template { - path := filepath.Join(*goroot, "lib", "godoc", name) + path := "lib/godoc/" + name if *templateDir != "" { defaultpath := path - path = filepath.Join(*templateDir, name) + path = pathpkg.Join(*templateDir, name) if _, err := fs.Stat(path); err != nil { log.Print("readTemplate:", err) path = defaultpath @@ -718,20 +546,23 @@ func readTemplates() { // ---------------------------------------------------------------------------- // Generic HTML wrapper -func servePage(w http.ResponseWriter, title, subtitle, query string, content []byte) { +func servePage(w http.ResponseWriter, tabtitle, title, subtitle, query string, content []byte) { + if tabtitle == "" { + tabtitle = title + } d := struct { + Tabtitle string Title string Subtitle string - PkgRoots []string SearchBox bool Query string Version string Menu []byte Content []byte }{ + tabtitle, title, subtitle, - fsMap.PrefixList(), *indexEnabled, query, runtime.Version(), @@ -780,6 +611,23 @@ func serveHTMLDoc(w http.ResponseWriter, r *http.Request, abspath, relpath strin 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 @@ -787,7 +635,7 @@ func serveHTMLDoc(w http.ResponseWriter, r *http.Request, abspath, relpath strin src = buf.Bytes() } - servePage(w, meta.Title, meta.Subtitle, "", src) + servePage(w, "", meta.Title, meta.Subtitle, "", src) } func applyTemplate(t *template.Template, name string, data interface{}) []byte { @@ -799,7 +647,7 @@ func applyTemplate(t *template.Template, name string, data interface{}) []byte { } func redirect(w http.ResponseWriter, r *http.Request) (redirected bool) { - canonical := path.Clean(r.URL.Path) + canonical := pathpkg.Clean(r.URL.Path) if !strings.HasSuffix("/", canonical) { canonical += "/" } @@ -820,10 +668,10 @@ func serveTextFile(w http.ResponseWriter, r *http.Request, abspath, relpath, tit var buf bytes.Buffer buf.WriteString("<pre>") - FormatText(&buf, src, 1, filepath.Ext(abspath) == ".go", r.FormValue("h"), rangeSelection(r.FormValue("s"))) + FormatText(&buf, src, 1, pathpkg.Ext(abspath) == ".go", r.FormValue("h"), rangeSelection(r.FormValue("s"))) buf.WriteString("</pre>") - servePage(w, title+" "+relpath, "", "", buf.Bytes()) + servePage(w, relpath, title+" "+relpath, "", "", buf.Bytes()) } func serveDirectory(w http.ResponseWriter, r *http.Request, abspath, relpath string) { @@ -833,13 +681,12 @@ func serveDirectory(w http.ResponseWriter, r *http.Request, abspath, relpath str list, err := fs.ReadDir(abspath) if err != nil { - log.Printf("ReadDir: %s", err) serveError(w, r, relpath, err) return } contents := applyTemplate(dirlistHTML, "dirlistHTML", list) - servePage(w, "Directory "+relpath, "", "", contents) + servePage(w, relpath, "Directory "+relpath, "", "", contents) } func serveFile(w http.ResponseWriter, r *http.Request) { @@ -856,10 +703,10 @@ func serveFile(w http.ResponseWriter, r *http.Request) { relpath = m.filePath } + abspath := relpath relpath = relpath[1:] // strip leading slash - abspath := absolutePath(relpath, *goroot) - switch path.Ext(relpath) { + switch pathpkg.Ext(relpath) { case ".html": if strings.HasSuffix(relpath, "/index.html") { // We'll show index.html for the directory. @@ -886,8 +733,8 @@ func serveFile(w http.ResponseWriter, r *http.Request) { if redirect(w, r) { return } - if index := filepath.Join(abspath, "index.html"); isTextFile(index) { - serveHTMLDoc(w, r, index, relativeURL(index)) + if index := pathpkg.Join(abspath, "index.html"); isTextFile(index) { + serveHTMLDoc(w, r, index, index) return } serveDirectory(w, r, abspath, relpath) @@ -992,7 +839,7 @@ func (info *PageInfo) IsEmpty() bool { return info.Err != nil || info.PAst == nil && info.PDoc == nil && info.Dirs == nil } -type httpHandler struct { +type docServer struct { pattern string // url pattern; e.g. "/pkg/" fsRoot string // file system root to which the pattern is mapped isPkg bool // true if this handler serves real package documentation (as opposed to command documentation) @@ -1029,7 +876,7 @@ func inList(name string, list []string) bool { // directories, PageInfo.Dirs is nil. If a directory read error occurred, // PageInfo.Err is set to the respective error but the error is not logged. // -func (h *httpHandler) getPageInfo(abspath, relpath, pkgname string, mode PageInfoMode) PageInfo { +func (h *docServer) getPageInfo(abspath, relpath, pkgname string, mode PageInfoMode) PageInfo { var pkgFiles []string // If we're showing the default package, restrict to the ones @@ -1043,7 +890,7 @@ func (h *httpHandler) getPageInfo(abspath, relpath, pkgname string, mode PageInf // to choose, set ctxt.GOOS and ctxt.GOARCH before // calling ctxt.ScanDir. ctxt := build.Default - ctxt.IsAbsPath = path.IsAbs + ctxt.IsAbsPath = pathpkg.IsAbs ctxt.ReadDir = fsReadDir ctxt.OpenFile = fsOpenFile dir, err := ctxt.ImportDir(abspath, 0) @@ -1091,13 +938,13 @@ func (h *httpHandler) getPageInfo(abspath, relpath, pkgname string, mode PageInf // the package with dirname, and the 3rd choice is a package // that is not called "main" if there is exactly one such // package. Otherwise, don't select a package. - dirpath, dirname := filepath.Split(abspath) + dirpath, dirname := pathpkg.Split(abspath) // If the dirname is "go" we might be in a sub-directory for // .go files - use the outer directory name instead for better // results. if dirname == "go" { - _, dirname = filepath.Split(filepath.Clean(dirpath)) + _, dirname = pathpkg.Split(pathpkg.Clean(dirpath)) } var choice3 *ast.Package @@ -1161,7 +1008,7 @@ func (h *httpHandler) getPageInfo(abspath, relpath, pkgname string, mode PageInf if mode&allMethods != 0 { m |= doc.AllMethods } - pdoc = doc.New(pkg, path.Clean(relpath), m) // no trailing '/' in importpath + pdoc = doc.New(pkg, pathpkg.Clean(relpath), m) // no trailing '/' in importpath } else { // show source code // TODO(gri) Consider eliminating export filtering in this mode, @@ -1184,34 +1031,11 @@ func (h *httpHandler) getPageInfo(abspath, relpath, pkgname string, mode PageInf timestamp = ts } if dir == nil { - // the path may refer to a user-specified file system mapped - // via fsMap; lookup that mapping and corresponding RWValue - // if any - var v *RWValue - fsMap.Iterate(func(path string, value *RWValue) bool { - if isParentOf(path, abspath) { - // mapping found - v = value - return false - } - return true - }) - if v != nil { - // found a RWValue associated with a user-specified file - // system; a non-nil RWValue stores a (possibly out-of-date) - // directory tree for that file system - if tree, ts := v.get(); tree != nil && tree.(*Directory) != nil { - 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, nil, 1) + dir = newDirectory(abspath, 1) timestamp = time.Now() } @@ -1230,13 +1054,13 @@ func (h *httpHandler) getPageInfo(abspath, relpath, pkgname string, mode PageInf } } -func (h *httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { +func (h *docServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { if redirect(w, r) { return } - relpath := path.Clean(r.URL.Path[len(h.pattern):]) - abspath := absolutePath(relpath, h.fsRoot) + relpath := pathpkg.Clean(r.URL.Path[len(h.pattern):]) + abspath := pathpkg.Join(h.fsRoot, relpath) mode := getPageInfoMode(r) if relpath == builtinPkgPath { mode = noFiltering @@ -1254,30 +1078,41 @@ func (h *httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - var title, subtitle string + var tabtitle, title, subtitle string switch { case info.PAst != nil: - title = "Package " + info.PAst.Name.Name + tabtitle = info.PAst.Name.Name + title = "Package " + tabtitle case info.PDoc != nil: - switch { - case info.IsPkg: - title = "Package " + info.PDoc.Name - case info.PDoc.Name == fakePkgName: + if info.PDoc.Name == fakePkgName { // assume that the directory name is the command name - _, pkgname := path.Split(relpath) - title = "Command " + pkgname - default: - title = "Command " + info.PDoc.Name + _, tabtitle = pathpkg.Split(relpath) + } else { + tabtitle = info.PDoc.Name + } + if info.IsPkg { + title = "Package " + tabtitle + } else { + title = "Command " + tabtitle } default: - title = "Directory " + relativeURL(info.Dirname) + tabtitle = info.Dirname + title = "Directory " + tabtitle if *showTimestamps { subtitle = "Last update: " + info.DirTime.String() } } + // special cases for top-level package/command directories + switch tabtitle { + case "/src/pkg": + tabtitle = "Packages" + case "/src/cmd": + tabtitle = "Commands" + } + contents := applyTemplate(packageHTML, "packageHTML", info) - servePage(w, title, subtitle, "", contents) + servePage(w, tabtitle, title, subtitle, "", contents) } // ---------------------------------------------------------------------------- @@ -1367,7 +1202,7 @@ func search(w http.ResponseWriter, r *http.Request) { } contents := applyTemplate(searchHTML, "searchHTML", result) - servePage(w, title, "", query, contents) + servePage(w, query, title, "", query, contents) } // ---------------------------------------------------------------------------- @@ -1376,6 +1211,7 @@ func search(w http.ResponseWriter, r *http.Request) { type Metadata struct { Title string Subtitle string + Template bool // execute as template Path string // canonical path for this page filePath string // filesystem path relative to goroot } @@ -1414,7 +1250,7 @@ func updateMetadata() { return } for _, fi := range fis { - name := filepath.Join(dir, fi.Name()) + name := pathpkg.Join(dir, fi.Name()) if fi.IsDir() { scan(name) // recurse continue @@ -1434,7 +1270,7 @@ func updateMetadata() { continue } // Store relative filesystem path in Metadata. - meta.filePath = filepath.Join("/", name[len(*goroot):]) + meta.filePath = name if meta.Path == "" { // If no Path, canonical path is actual path. meta.Path = meta.filePath @@ -1444,7 +1280,7 @@ func updateMetadata() { metadata[meta.filePath] = &meta } } - scan(filepath.Join(*goroot, "doc")) + scan("/doc") docMetadata.set(metadata) } @@ -1519,13 +1355,9 @@ func feedDirnames(root *RWValue, c chan<- string) { // of all the file systems under godoc's observation. // func fsDirnames() <-chan string { - c := make(chan string, 256) // asynchronous for fewer context switches + c := make(chan string, 256) // buffered for fewer context switches go func() { feedDirnames(&fsTree, c) - fsMap.Iterate(func(_ string, root *RWValue) bool { - feedDirnames(root, c) - return true - }) close(c) }() return c |