godoc: add a Corpus and Presentation type

Start of moving globals into those types.

R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/11484043
This commit is contained in:
Brad Fitzpatrick 2013-07-18 09:52:45 +10:00
parent 3af65e49c2
commit 4fc6323385
11 changed files with 221 additions and 117 deletions

View File

@ -62,7 +62,7 @@ func codewalk(w http.ResponseWriter, r *http.Request) {
cw, err := loadCodewalk(abspath + ".xml") cw, err := loadCodewalk(abspath + ".xml")
if err != nil { if err != nil {
log.Print(err) log.Print(err)
godoc.ServeError(w, r, relpath, err) pres.ServeError(w, r, relpath, err)
return return
} }
@ -71,7 +71,7 @@ func codewalk(w http.ResponseWriter, r *http.Request) {
return return
} }
godoc.ServePage(w, godoc.Page{ pres.ServePage(w, godoc.Page{
Title: "Codewalk: " + cw.Title, Title: "Codewalk: " + cw.Title,
Tabtitle: cw.Title, Tabtitle: cw.Title,
Body: applyTemplate(codewalkHTML, "codewalk", cw), Body: applyTemplate(codewalkHTML, "codewalk", cw),
@ -188,7 +188,7 @@ func codewalkDir(w http.ResponseWriter, r *http.Request, relpath, abspath string
dir, err := godoc.FS.ReadDir(abspath) dir, err := godoc.FS.ReadDir(abspath)
if err != nil { if err != nil {
log.Print(err) log.Print(err)
godoc.ServeError(w, r, relpath, err) pres.ServeError(w, r, relpath, err)
return return
} }
var v []interface{} var v []interface{}
@ -205,7 +205,7 @@ func codewalkDir(w http.ResponseWriter, r *http.Request, relpath, abspath string
} }
} }
godoc.ServePage(w, godoc.Page{ pres.ServePage(w, godoc.Page{
Title: "Codewalks", Title: "Codewalks",
Body: applyTemplate(codewalkdirHTML, "codewalkdir", v), Body: applyTemplate(codewalkdirHTML, "codewalkdir", v),
}) })
@ -222,7 +222,7 @@ func codewalkFileprint(w http.ResponseWriter, r *http.Request, f string) {
data, err := vfs.ReadFile(godoc.FS, abspath) data, err := vfs.ReadFile(godoc.FS, abspath)
if err != nil { if err != nil {
log.Print(err) log.Print(err)
godoc.ServeError(w, r, f, err) pres.ServeError(w, r, f, err)
return return
} }
lo, _ := strconv.Atoi(r.FormValue("lo")) lo, _ := strconv.Atoi(r.FormValue("lo"))

View File

@ -9,16 +9,12 @@ import (
"flag" "flag"
"fmt" "fmt"
htmlpkg "html" htmlpkg "html"
"log" "log"
"net/http" "net/http"
"net/url" "net/url"
pathpkg "path" pathpkg "path"
"regexp" "regexp"
"runtime" "runtime"
"strings" "strings"
"text/template" "text/template"
@ -55,8 +51,7 @@ var (
// file system roots // file system roots
// TODO(gri) consider the invariant that goroot always end in '/' // TODO(gri) consider the invariant that goroot always end in '/'
goroot = flag.String("goroot", runtime.GOROOT(), "Go root directory") goroot = flag.String("goroot", runtime.GOROOT(), "Go root directory")
testDir = flag.String("testdir", "", "Go root subdirectory - for testing only (faster startups)")
// layout control // layout control
_ = flagInt(&godoc.TabWidth, "tabwidth", 4, "tab width") _ = flagInt(&godoc.TabWidth, "tabwidth", 4, "tab width")
@ -67,8 +62,8 @@ var (
_ = flagBool(&godoc.DeclLinks, "links", true, "link identifiers to their declarations") _ = flagBool(&godoc.DeclLinks, "links", true, "link identifiers to their declarations")
// search index // search index
_ = flagBool(&godoc.IndexEnabled, "index", false, "enable search index") indexEnabled = flag.Bool("index", false, "enable search index")
_ = flagString(&godoc.IndexFiles, "index_files", "", "glob pattern specifying index files;"+ 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")
_ = flagInt(&godoc.MaxResults, "maxresults", 10000, "maximum number of full text search results shown") _ = flagInt(&godoc.MaxResults, "maxresults", 10000, "maximum number of full text search results shown")
_ = flagFloat64(&godoc.IndexThrottle, "index_throttle", 0.75, "index throttle value; 0.0 = no time allocated, 1.0 = full throttle") _ = flagFloat64(&godoc.IndexThrottle, "index_throttle", 0.75, "index throttle value; 0.0 = no time allocated, 1.0 = full throttle")
@ -77,6 +72,8 @@ var (
_ = flagString(&godoc.NotesRx, "notes", "BUG", "regular expression matching note markers to show") _ = flagString(&godoc.NotesRx, "notes", "BUG", "regular expression matching note markers to show")
) )
var pres *godoc.Presentation
func registerPublicHandlers(mux *http.ServeMux) { func registerPublicHandlers(mux *http.ServeMux) {
godoc.CmdHandler.RegisterWithMux(mux) godoc.CmdHandler.RegisterWithMux(mux)
godoc.PkgHandler.RegisterWithMux(mux) godoc.PkgHandler.RegisterWithMux(mux)
@ -88,16 +85,6 @@ func registerPublicHandlers(mux *http.ServeMux) {
mux.HandleFunc("/", serveFile) mux.HandleFunc("/", serveFile)
} }
func initFSTree() {
dir := godoc.NewDirectory(pathpkg.Join("/", *testDir), -1)
if dir == nil {
log.Println("Warning: FSTree is nil")
return
}
godoc.FSTree.Set(dir)
godoc.InvalidateIndex()
}
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Templates // Templates
@ -176,12 +163,12 @@ func serveTextFile(w http.ResponseWriter, r *http.Request, abspath, relpath, tit
src, err := vfs.ReadFile(godoc.FS, abspath) src, err := vfs.ReadFile(godoc.FS, abspath)
if err != nil { if err != nil {
log.Printf("ReadFile: %s", err) log.Printf("ReadFile: %s", err)
godoc.ServeError(w, r, relpath, err) pres.ServeError(w, r, relpath, err)
return return
} }
if r.FormValue("m") == "text" { if r.FormValue("m") == "text" {
godoc.ServeText(w, src) pres.ServeText(w, src)
return return
} }
@ -191,7 +178,7 @@ func serveTextFile(w http.ResponseWriter, r *http.Request, abspath, relpath, tit
buf.WriteString("</pre>") buf.WriteString("</pre>")
fmt.Fprintf(&buf, `<p><a href="/%s?m=text">View as plain text</a></p>`, htmlpkg.EscapeString(relpath)) fmt.Fprintf(&buf, `<p><a href="/%s?m=text">View as plain text</a></p>`, htmlpkg.EscapeString(relpath))
godoc.ServePage(w, godoc.Page{ pres.ServePage(w, godoc.Page{
Title: title + " " + relpath, Title: title + " " + relpath,
Tabtitle: relpath, Tabtitle: relpath,
Body: buf.Bytes(), Body: buf.Bytes(),
@ -205,11 +192,11 @@ func serveDirectory(w http.ResponseWriter, r *http.Request, abspath, relpath str
list, err := godoc.FS.ReadDir(abspath) list, err := godoc.FS.ReadDir(abspath)
if err != nil { if err != nil {
godoc.ServeError(w, r, relpath, err) pres.ServeError(w, r, relpath, err)
return return
} }
godoc.ServePage(w, godoc.Page{ pres.ServePage(w, godoc.Page{
Title: "Directory " + relpath, Title: "Directory " + relpath,
Tabtitle: relpath, Tabtitle: relpath,
Body: applyTemplate(godoc.DirlistHTML, "dirlistHTML", list), Body: applyTemplate(godoc.DirlistHTML, "dirlistHTML", list),
@ -241,7 +228,7 @@ func serveFile(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, r.URL.Path[0:len(r.URL.Path)-len("index.html")], http.StatusMovedPermanently) http.Redirect(w, r, r.URL.Path[0:len(r.URL.Path)-len("index.html")], http.StatusMovedPermanently)
return return
} }
godoc.ServeHTMLDoc(w, r, abspath, relpath) pres.ServeHTMLDoc(w, r, abspath, relpath)
return return
case ".go": case ".go":
@ -252,7 +239,7 @@ func serveFile(w http.ResponseWriter, r *http.Request) {
dir, err := godoc.FS.Lstat(abspath) dir, err := godoc.FS.Lstat(abspath)
if err != nil { if err != nil {
log.Print(err) log.Print(err)
godoc.ServeError(w, r, relpath, err) pres.ServeError(w, r, relpath, err)
return return
} }
@ -261,7 +248,7 @@ func serveFile(w http.ResponseWriter, r *http.Request) {
return return
} }
if index := pathpkg.Join(abspath, "index.html"); util.IsTextFile(godoc.FS, index) { if index := pathpkg.Join(abspath, "index.html"); util.IsTextFile(godoc.FS, index) {
godoc.ServeHTMLDoc(w, r, index, index) pres.ServeHTMLDoc(w, r, index, index)
return return
} }
serveDirectory(w, r, abspath, relpath) serveDirectory(w, r, abspath, relpath)
@ -362,7 +349,7 @@ func lookup(query string) (result SearchResult) {
} }
// is the result accurate? // is the result accurate?
if godoc.IndexEnabled { if pres.Corpus.IndexEnabled {
if _, ts := godoc.FSModified.Get(); timestamp.Before(ts) { if _, ts := godoc.FSModified.Get(); timestamp.Before(ts) {
// The index is older than the latest file system change under godoc's observation. // The index is older than the latest file system change under godoc's observation.
result.Alert = "Indexing in progress: result may be inaccurate" result.Alert = "Indexing in progress: result may be inaccurate"
@ -379,7 +366,7 @@ func search(w http.ResponseWriter, r *http.Request) {
result := lookup(query) result := lookup(query)
if godoc.GetPageInfoMode(r)&godoc.NoHTML != 0 { if godoc.GetPageInfoMode(r)&godoc.NoHTML != 0 {
godoc.ServeText(w, applyTemplate(godoc.SearchText, "searchText", result)) pres.ServeText(w, applyTemplate(godoc.SearchText, "searchText", result))
return return
} }
@ -390,7 +377,7 @@ func search(w http.ResponseWriter, r *http.Request) {
title = fmt.Sprintf(`No results found for query %q`, query) title = fmt.Sprintf(`No results found for query %q`, query)
} }
godoc.ServePage(w, godoc.Page{ pres.ServePage(w, godoc.Page{
Title: title, Title: title,
Tabtitle: query, Tabtitle: query,
Query: query, Query: query,

View File

@ -158,9 +158,8 @@ func main() {
usage() usage()
} }
if godoc.TabWidth < 0 { fs := vfs.NameSpace{}
log.Fatalf("negative tabwidth %d", godoc.TabWidth) godoc.FS = fs // temp hack
}
// Determine file system to use. // Determine file system to use.
// TODO(gri) - fs and fsHttp should really be the same. Try to unify. // TODO(gri) - fs and fsHttp should really be the same. Try to unify.
@ -168,9 +167,9 @@ func main() {
// same is true for the http handlers in initHandlers. // same is true for the http handlers in initHandlers.
if *zipfile == "" { if *zipfile == "" {
// use file system of underlying OS // use file system of underlying OS
godoc.FS.Bind("/", vfs.OS(*goroot), "/", vfs.BindReplace) fs.Bind("/", vfs.OS(*goroot), "/", vfs.BindReplace)
if *templateDir != "" { if *templateDir != "" {
godoc.FS.Bind("/lib/godoc", vfs.OS(*templateDir), "/", vfs.BindBefore) fs.Bind("/lib/godoc", vfs.OS(*templateDir), "/", vfs.BindBefore)
} }
} else { } else {
// use file system specified via .zip file (path separator must be '/') // use file system specified via .zip file (path separator must be '/')
@ -179,16 +178,23 @@ func main() {
log.Fatalf("%s: %s\n", *zipfile, err) log.Fatalf("%s: %s\n", *zipfile, err)
} }
defer rc.Close() // be nice (e.g., -writeIndex mode) defer rc.Close() // be nice (e.g., -writeIndex mode)
godoc.FS.Bind("/", zipvfs.New(rc, *zipfile), *goroot, vfs.BindReplace) fs.Bind("/", zipvfs.New(rc, *zipfile), *goroot, vfs.BindReplace)
} }
// Bind $GOPATH trees into Go root. // Bind $GOPATH trees into Go root.
for _, p := range filepath.SplitList(build.Default.GOPATH) { for _, p := range filepath.SplitList(build.Default.GOPATH) {
godoc.FS.Bind("/src/pkg", vfs.OS(p), "/src", vfs.BindAfter) fs.Bind("/src/pkg", vfs.OS(p), "/src", vfs.BindAfter)
} }
readTemplates() readTemplates()
godoc.InitHandlers(godoc.FS)
corpus := godoc.NewCorpus(fs)
corpus.IndexEnabled = *indexEnabled
corpus.IndexFiles = *indexFiles
pres = godoc.NewPresentation(corpus)
// ...
godoc.InitHandlers(pres)
if *writeIndex { if *writeIndex {
// Write search index and exit. // Write search index and exit.
@ -198,7 +204,6 @@ func main() {
log.Println("initialize file systems") log.Println("initialize file systems")
*verbose = true // want to see what happens *verbose = true // want to see what happens
initFSTree()
godoc.IndexThrottle = 1.0 godoc.IndexThrottle = 1.0
godoc.UpdateIndex() godoc.UpdateIndex()
@ -221,8 +226,7 @@ func main() {
// Print content that would be served at the URL *urlFlag. // Print content that would be served at the URL *urlFlag.
if *urlFlag != "" { if *urlFlag != "" {
registerPublicHandlers(http.DefaultServeMux) registerPublicHandlers(http.DefaultServeMux)
initFSTree()
godoc.UpdateMetadata()
// Try up to 10 fetches, following redirects. // Try up to 10 fetches, following redirects.
urlstr := *urlFlag urlstr := *urlFlag
for i := 0; i < 10; i++ { for i := 0; i < 10; i++ {
@ -268,30 +272,21 @@ func main() {
log.Printf("goroot = %s", *goroot) log.Printf("goroot = %s", *goroot)
log.Printf("tabwidth = %d", godoc.TabWidth) log.Printf("tabwidth = %d", godoc.TabWidth)
switch { switch {
case !godoc.IndexEnabled: case !*indexEnabled:
log.Print("search index disabled") log.Print("search index disabled")
case godoc.MaxResults > 0: case godoc.MaxResults > 0:
log.Printf("full text index enabled (maxresults = %d)", godoc.MaxResults) log.Printf("full text index enabled (maxresults = %d)", godoc.MaxResults)
default: default:
log.Print("identifier search index enabled") log.Print("identifier search index enabled")
} }
godoc.FS.Fprint(os.Stderr) fs.Fprint(os.Stderr)
handler = loggingHandler(handler) handler = loggingHandler(handler)
} }
registerPublicHandlers(http.DefaultServeMux) registerPublicHandlers(http.DefaultServeMux)
// Initialize default directory tree with corresponding timestamp.
// (Do it in a goroutine so that launch is quick.)
go initFSTree()
// Immediately update metadata.
godoc.UpdateMetadata()
// Periodically refresh metadata.
go godoc.RefreshMetadataLoop()
// Initialize search index. // Initialize search index.
if godoc.IndexEnabled { if *indexEnabled {
go godoc.RunIndexer() go godoc.RunIndexer()
} }
@ -335,18 +330,18 @@ func main() {
var forceCmd bool var forceCmd bool
var abspath, relpath string var abspath, relpath string
if filepath.IsAbs(path) { if filepath.IsAbs(path) {
godoc.FS.Bind(target, vfs.OS(path), "/", vfs.BindReplace) fs.Bind(target, vfs.OS(path), "/", vfs.BindReplace)
abspath = target abspath = target
} else if build.IsLocalImport(path) { } else if build.IsLocalImport(path) {
cwd, _ := os.Getwd() // ignore errors cwd, _ := os.Getwd() // ignore errors
path = filepath.Join(cwd, path) path = filepath.Join(cwd, path)
godoc.FS.Bind(target, vfs.OS(path), "/", vfs.BindReplace) fs.Bind(target, vfs.OS(path), "/", vfs.BindReplace)
abspath = target abspath = target
} else if strings.HasPrefix(path, cmdPrefix) { } else if strings.HasPrefix(path, cmdPrefix) {
path = strings.TrimPrefix(path, cmdPrefix) path = strings.TrimPrefix(path, cmdPrefix)
forceCmd = true forceCmd = true
} else if bp, _ := build.Import(path, "", build.FindOnly); bp.Dir != "" && bp.ImportPath != "" { } else if bp, _ := build.Import(path, "", build.FindOnly); bp.Dir != "" && bp.ImportPath != "" {
godoc.FS.Bind(target, vfs.OS(bp.Dir), "/", vfs.BindReplace) fs.Bind(target, vfs.OS(bp.Dir), "/", vfs.BindReplace)
abspath = target abspath = target
relpath = bp.ImportPath relpath = bp.ImportPath
} else { } else {

85
godoc/corpus.go Normal file
View File

@ -0,0 +1,85 @@
// 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 (
"errors"
pathpkg "path"
"code.google.com/p/go.tools/godoc/util"
"code.google.com/p/go.tools/godoc/vfs"
)
// A Corpus holds all the state related to serving and indexing a
// collection of Go code.
//
// Construct a new Corpus with NewCorpus, then modify options,
// then call its Init method.
type Corpus struct {
fs vfs.FileSystem
// IndexEnabled controls whether indexing is enabled.
IndexEnabled bool
// IndexFiles specifies a glob pattern specifying index files.
// If not empty, the index is read from these files in sorted
// order.
IndexFiles string
IndexThrottle float64
// MaxResults optionally specifies the maximum results for indexing.
// The default is 1000.
MaxResults int
testDir string // TODO(bradfitz,adg): migrate old godoc flag? looks unused.
// Send a value on this channel to trigger a metadata refresh.
// It is buffered so that if a signal is not lost if sent
// during a refresh.
refreshMetadataSignal chan bool
// 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
}
// NewCorpus returns a new Corpus from a filesystem.
// Set any options on Corpus before calling the Corpus.Init method.
func NewCorpus(fs vfs.FileSystem) *Corpus {
c := &Corpus{
fs: fs,
refreshMetadataSignal: make(chan bool, 1),
MaxResults: 1000,
IndexEnabled: true,
}
return c
}
// Init initializes Corpus, once options on Corpus are set.
// It must be called before any subsequent method calls.
func (c *Corpus) Init() error {
// TODO(bradfitz): do this in a goroutine because newDirectory might block for a long time?
// It used to be sometimes done in a goroutine before, at least in HTTP server mode.
if err := c.initFSTree(); err != nil {
return err
}
c.updateMetadata()
go c.refreshMetadataLoop()
return nil
}
func (c *Corpus) initFSTree() error {
dir := c.newDirectory(pathpkg.Join("/", c.testDir), -1)
if dir == nil {
return errors.New("godoc: corpus fstree is nil")
}
c.fsTree.Set(dir)
c.invalidateIndex()
return nil
}

View File

@ -50,6 +50,7 @@ func isPkgDir(fi os.FileInfo) bool {
} }
type treeBuilder struct { type treeBuilder struct {
c *Corpus
maxDepth int maxDepth int
} }
@ -69,7 +70,7 @@ func (b *treeBuilder) newDirTree(fset *token.FileSet, path, name string, depth i
} }
} }
list, _ := FS.ReadDir(path) list, _ := b.c.fs.ReadDir(path)
// determine number of subdirectories and if there are package files // determine number of subdirectories and if there are package files
ndirs := 0 ndirs := 0
@ -161,9 +162,9 @@ func (b *treeBuilder) newDirTree(fset *token.FileSet, path, name string, depth i
// are assumed to contain package files even if their contents are not known // are assumed to contain package files even if their contents are not known
// (i.e., in this case the tree may contain directories w/o any package files). // (i.e., in this case the tree may contain directories w/o any package files).
// //
func NewDirectory(root string, maxDepth int) *Directory { func (c *Corpus) newDirectory(root string, maxDepth int) *Directory {
// The root could be a symbolic link so use Stat not Lstat. // The root could be a symbolic link so use Stat not Lstat.
d, err := FS.Stat(root) d, err := c.fs.Stat(root)
// If we fail here, report detailed error messages; otherwise // If we fail here, report detailed error messages; otherwise
// is is hard to see why a directory tree was not built. // is is hard to see why a directory tree was not built.
switch { switch {
@ -177,7 +178,7 @@ func NewDirectory(root string, maxDepth int) *Directory {
if maxDepth < 0 { if maxDepth < 0 {
maxDepth = 1e6 // "infinity" maxDepth = 1e6 // "infinity"
} }
b := treeBuilder{maxDepth} b := treeBuilder{c, maxDepth}
// the file set provided is only for local parsing, no position // the file set provided is only for local parsing, no position
// information escapes and thus we don't need to save the set // information escapes and thus we don't need to save the set
return b.newDirTree(token.NewFileSet(), root, d.Name(), 0) return b.newDirTree(token.NewFileSet(), root, d.Name(), 0)

View File

@ -38,6 +38,7 @@ import (
// holds the source tree, and so on. This means that the URLs served by // holds the source tree, and so on. This means that the URLs served by
// the godoc server are the same as the paths in the virtual file // the godoc server are the same as the paths in the virtual file
// system, which helps keep things simple. // system, which helps keep things simple.
// TODO(bradfitz): delete this global
var FS = vfs.NameSpace{} var FS = vfs.NameSpace{}
// Old flags // Old flags
@ -54,8 +55,6 @@ var (
// TODO(bradfitz,adg): delete this flag // TODO(bradfitz,adg): delete this flag
ShowPlayground = false ShowPlayground = false
IndexEnabled = false
ShowTimestamps = false ShowTimestamps = false
Verbose = false Verbose = false

View File

@ -1038,9 +1038,9 @@ func (x *Index) LookupRegexp(r *regexp.Regexp, n int) (found int, result []FileL
// InvalidateIndex should be called whenever any of the file systems // InvalidateIndex should be called whenever any of the file systems
// under godoc's observation change so that the indexer is kicked on. // under godoc's observation change so that the indexer is kicked on.
func InvalidateIndex() { func (c *Corpus) invalidateIndex() {
FSModified.Set(nil) c.fsModified.Set(nil)
refreshMetadata() c.refreshMetadata()
} }
// indexUpToDate() returns true if the search index is not older // indexUpToDate() returns true if the search index is not older

View File

@ -58,11 +58,11 @@ func extractMetadata(b []byte) (meta Metadata, tail []byte, err error) {
// UpdateMetadata scans $GOROOT/doc for HTML files, reads their metadata, // UpdateMetadata scans $GOROOT/doc for HTML files, reads their metadata,
// and updates the DocMetadata map. // and updates the DocMetadata map.
func UpdateMetadata() { func (c *Corpus) updateMetadata() {
metadata := make(map[string]*Metadata) metadata := make(map[string]*Metadata)
var scan func(string) // scan is recursive var scan func(string) // scan is recursive
scan = func(dir string) { scan = func(dir string) {
fis, err := FS.ReadDir(dir) fis, err := c.fs.ReadDir(dir)
if err != nil { if err != nil {
log.Println("updateMetadata:", err) log.Println("updateMetadata:", err)
return return
@ -77,7 +77,7 @@ func UpdateMetadata() {
continue continue
} }
// Extract metadata from the file. // Extract metadata from the file.
b, err := vfs.ReadFile(FS, name) b, err := vfs.ReadFile(c.fs, name)
if err != nil { if err != nil {
log.Printf("updateMetadata %s: %v", name, err) log.Printf("updateMetadata %s: %v", name, err)
continue continue
@ -123,27 +123,22 @@ func MetadataFor(relpath string) *Metadata {
return nil return nil
} }
// Send a value on this channel to trigger a metadata refresh.
// It is buffered so that if a signal is not lost if sent during a refresh.
//
var refreshMetadataSignal = make(chan bool, 1)
// refreshMetadata sends a signal to update DocMetadata. If a refresh is in // refreshMetadata sends a signal to update DocMetadata. If a refresh is in
// progress the metadata will be refreshed again afterward. // progress the metadata will be refreshed again afterward.
// //
func refreshMetadata() { func (c *Corpus) refreshMetadata() {
select { select {
case refreshMetadataSignal <- true: case c.refreshMetadataSignal <- true:
default: default:
} }
} }
// RefreshMetadataLoop runs forever, updating DocMetadata when the underlying // RefreshMetadataLoop runs forever, updating DocMetadata when the underlying
// file system changes. It should be launched in a goroutine. // file system changes. It should be launched in a goroutine.
func RefreshMetadataLoop() { func (c *Corpus) refreshMetadataLoop() {
for { for {
<-refreshMetadataSignal <-c.refreshMetadataSignal
UpdateMetadata() c.updateMetadata()
time.Sleep(10 * time.Second) // at most once every 10 seconds time.Sleep(10 * time.Second) // at most once every 10 seconds
} }
} }

View File

@ -37,11 +37,11 @@ var (
SearchDescXML *template.Template SearchDescXML *template.Template
) )
func ServePage(w http.ResponseWriter, page Page) { func (p *Presentation) ServePage(w http.ResponseWriter, page Page) {
if page.Tabtitle == "" { if page.Tabtitle == "" {
page.Tabtitle = page.Title page.Tabtitle = page.Title
} }
page.SearchBox = IndexEnabled page.SearchBox = p.Corpus.IndexEnabled
page.Playground = ShowPlayground page.Playground = ShowPlayground
page.Version = runtime.Version() page.Version = runtime.Version()
if err := GodocHTML.Execute(w, page); err != nil && err != http.ErrBodyNotAllowed { if err := GodocHTML.Execute(w, page); err != nil && err != http.ErrBodyNotAllowed {
@ -51,9 +51,9 @@ func ServePage(w http.ResponseWriter, page Page) {
} }
} }
func ServeError(w http.ResponseWriter, r *http.Request, relpath string, err error) { func (p *Presentation) ServeError(w http.ResponseWriter, r *http.Request, relpath string, err error) {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
ServePage(w, Page{ p.ServePage(w, Page{
Title: "File " + relpath, Title: "File " + relpath,
Subtitle: relpath, Subtitle: relpath,
Body: applyTemplate(ErrorHTML, "errorHTML", err), // err may contain an absolute path! Body: applyTemplate(ErrorHTML, "errorHTML", err), // err may contain an absolute path!

39
godoc/pres.go Normal file
View File

@ -0,0 +1,39 @@
// 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 (
"text/template"
)
// Presentation generates output from a corpus.
type Presentation struct {
Corpus *Corpus
// TabWidth optionally specifies the tab width.
TabWidth int
ShowTimestamps bool
ShowPlayground bool
ShowExamples bool
DeclLinks bool
}
// NewPresentation returns a new Presentation from a corpus.
func NewPresentation(c *Corpus) *Presentation {
if c == nil {
panic("nil Corpus")
}
return &Presentation{
Corpus: c,
TabWidth: 4,
ShowExamples: true,
DeclLinks: true,
}
}
func (p *Presentation) FuncMap() template.FuncMap {
panic("")
}

View File

@ -42,16 +42,19 @@ var (
DocMetadata util.RWValue // mapping from paths to *Metadata DocMetadata util.RWValue // mapping from paths to *Metadata
) )
func InitHandlers(fs vfs.FileSystem) { func InitHandlers(p *Presentation) {
FileServer = http.FileServer(httpfs.New(fs)) c := p.Corpus
CmdHandler = Server{"/cmd/", "/src/cmd"} FileServer = http.FileServer(httpfs.New(c.fs))
PkgHandler = Server{"/pkg/", "/src/pkg"} CmdHandler = Server{p, c, "/cmd/", "/src/cmd"}
PkgHandler = Server{p, c, "/pkg/", "/src/pkg"}
} }
// Server is a godoc server. // Server is a godoc server.
type Server struct { type Server struct {
pattern string // url pattern; e.g. "/pkg/" p *Presentation
fsRoot string // file system root to which the pattern is mapped c *Corpus // copy of p.Corpus
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) FSRoot() string { return s.fsRoot }
@ -178,7 +181,7 @@ func (h *Server) GetPageInfo(abspath, relpath string, mode PageInfoMode) *PageIn
// command-line mode); compute one level for this page // command-line mode); compute one level for this page
// note: cannot use path filter here because in general // note: cannot use path filter here because in general
// it doesn't contain the FSTree path // it doesn't contain the FSTree path
dir = NewDirectory(abspath, 1) dir = h.c.newDirectory(abspath, 1)
timestamp = time.Now() timestamp = time.Now()
} }
info.Dirs = dir.listing(true) info.Dirs = dir.listing(true)
@ -202,12 +205,12 @@ func (h *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
info := h.GetPageInfo(abspath, relpath, mode) info := h.GetPageInfo(abspath, relpath, mode)
if info.Err != nil { if info.Err != nil {
log.Print(info.Err) log.Print(info.Err)
ServeError(w, r, relpath, info.Err) h.p.ServeError(w, r, relpath, info.Err)
return return
} }
if mode&NoHTML != 0 { if mode&NoHTML != 0 {
ServeText(w, applyTemplate(PackageText, "packageText", info)) h.p.ServeText(w, applyTemplate(PackageText, "packageText", info))
return return
} }
@ -243,7 +246,7 @@ func (h *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
tabtitle = "Commands" tabtitle = "Commands"
} }
ServePage(w, Page{ h.p.ServePage(w, Page{
Title: title, Title: title,
Tabtitle: tabtitle, Tabtitle: tabtitle,
Subtitle: subtitle, Subtitle: subtitle,
@ -431,16 +434,16 @@ func redirectFile(w http.ResponseWriter, r *http.Request) (redirected bool) {
return return
} }
func serveTextFile(w http.ResponseWriter, r *http.Request, abspath, relpath, title string) { func (p *Presentation) serveTextFile(w http.ResponseWriter, r *http.Request, abspath, relpath, title string) {
src, err := vfs.ReadFile(FS, abspath) src, err := vfs.ReadFile(p.Corpus.fs, abspath)
if err != nil { if err != nil {
log.Printf("ReadFile: %s", err) log.Printf("ReadFile: %s", err)
ServeError(w, r, relpath, err) p.ServeError(w, r, relpath, err)
return return
} }
if r.FormValue("m") == "text" { if r.FormValue("m") == "text" {
ServeText(w, src) p.ServeText(w, src)
return return
} }
@ -450,37 +453,37 @@ func serveTextFile(w http.ResponseWriter, r *http.Request, abspath, relpath, tit
buf.WriteString("</pre>") buf.WriteString("</pre>")
fmt.Fprintf(&buf, `<p><a href="/%s?m=text">View as plain text</a></p>`, htmlpkg.EscapeString(relpath)) fmt.Fprintf(&buf, `<p><a href="/%s?m=text">View as plain text</a></p>`, htmlpkg.EscapeString(relpath))
ServePage(w, Page{ p.ServePage(w, Page{
Title: title + " " + relpath, Title: title + " " + relpath,
Tabtitle: relpath, Tabtitle: relpath,
Body: buf.Bytes(), Body: buf.Bytes(),
}) })
} }
func serveDirectory(w http.ResponseWriter, r *http.Request, abspath, relpath string) { func (p *Presentation) serveDirectory(w http.ResponseWriter, r *http.Request, abspath, relpath string) {
if redirect(w, r) { if redirect(w, r) {
return return
} }
list, err := FS.ReadDir(abspath) list, err := FS.ReadDir(abspath)
if err != nil { if err != nil {
ServeError(w, r, relpath, err) p.ServeError(w, r, relpath, err)
return return
} }
ServePage(w, Page{ p.ServePage(w, Page{
Title: "Directory " + relpath, Title: "Directory " + relpath,
Tabtitle: relpath, Tabtitle: relpath,
Body: applyTemplate(DirlistHTML, "dirlistHTML", list), Body: applyTemplate(DirlistHTML, "dirlistHTML", list),
}) })
} }
func ServeHTMLDoc(w http.ResponseWriter, r *http.Request, abspath, relpath string) { func (p *Presentation) ServeHTMLDoc(w http.ResponseWriter, r *http.Request, abspath, relpath string) {
// get HTML body contents // get HTML body contents
src, err := vfs.ReadFile(FS, abspath) src, err := vfs.ReadFile(FS, abspath)
if err != nil { if err != nil {
log.Printf("ReadFile: %s", err) log.Printf("ReadFile: %s", err)
ServeError(w, r, relpath, err) p.ServeError(w, r, relpath, err)
return return
} }
@ -502,13 +505,13 @@ func ServeHTMLDoc(w http.ResponseWriter, r *http.Request, abspath, relpath strin
tmpl, err := template.New("main").Funcs(TemplateFuncs).Parse(string(src)) tmpl, err := template.New("main").Funcs(TemplateFuncs).Parse(string(src))
if err != nil { if err != nil {
log.Printf("parsing template %s: %v", relpath, err) log.Printf("parsing template %s: %v", relpath, err)
ServeError(w, r, relpath, err) p.ServeError(w, r, relpath, err)
return return
} }
var buf bytes.Buffer var buf bytes.Buffer
if err := tmpl.Execute(&buf, nil); err != nil { if err := tmpl.Execute(&buf, nil); err != nil {
log.Printf("executing template %s: %v", relpath, err) log.Printf("executing template %s: %v", relpath, err)
ServeError(w, r, relpath, err) p.ServeError(w, r, relpath, err)
return return
} }
src = buf.Bytes() src = buf.Bytes()
@ -521,14 +524,14 @@ func ServeHTMLDoc(w http.ResponseWriter, r *http.Request, abspath, relpath strin
src = buf.Bytes() src = buf.Bytes()
} }
ServePage(w, Page{ p.ServePage(w, Page{
Title: meta.Title, Title: meta.Title,
Subtitle: meta.Subtitle, Subtitle: meta.Subtitle,
Body: src, Body: src,
}) })
} }
func serveFile(w http.ResponseWriter, r *http.Request) { func (p *Presentation) serveFile(w http.ResponseWriter, r *http.Request) {
relpath := r.URL.Path relpath := r.URL.Path
// Check to see if we need to redirect or serve another file. // Check to see if we need to redirect or serve another file.
@ -553,18 +556,18 @@ func serveFile(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, r.URL.Path[0:len(r.URL.Path)-len("index.html")], http.StatusMovedPermanently) http.Redirect(w, r, r.URL.Path[0:len(r.URL.Path)-len("index.html")], http.StatusMovedPermanently)
return return
} }
ServeHTMLDoc(w, r, abspath, relpath) p.ServeHTMLDoc(w, r, abspath, relpath)
return return
case ".go": case ".go":
serveTextFile(w, r, abspath, relpath, "Source file") p.serveTextFile(w, r, abspath, relpath, "Source file")
return return
} }
dir, err := FS.Lstat(abspath) dir, err := FS.Lstat(abspath)
if err != nil { if err != nil {
log.Print(err) log.Print(err)
ServeError(w, r, relpath, err) p.ServeError(w, r, relpath, err)
return return
} }
@ -573,10 +576,10 @@ func serveFile(w http.ResponseWriter, r *http.Request) {
return return
} }
if index := pathpkg.Join(abspath, "index.html"); util.IsTextFile(FS, index) { if index := pathpkg.Join(abspath, "index.html"); util.IsTextFile(FS, index) {
ServeHTMLDoc(w, r, index, index) p.ServeHTMLDoc(w, r, index, index)
return return
} }
serveDirectory(w, r, abspath, relpath) p.serveDirectory(w, r, abspath, relpath)
return return
} }
@ -584,7 +587,7 @@ func serveFile(w http.ResponseWriter, r *http.Request) {
if redirectFile(w, r) { if redirectFile(w, r) {
return return
} }
serveTextFile(w, r, abspath, relpath, "Text file") p.serveTextFile(w, r, abspath, relpath, "Text file")
return return
} }
@ -603,7 +606,7 @@ func serveSearchDesc(w http.ResponseWriter, r *http.Request) {
} }
} }
func ServeText(w http.ResponseWriter, text []byte) { func (p *Presentation) ServeText(w http.ResponseWriter, text []byte) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Write(text) w.Write(text)
} }