]> Cypherpunks repositories - gostls13.git/commitdiff
godoc support for directories outside $GOROOT
authorRobert Griesemer <gri@golang.org>
Tue, 16 Feb 2010 19:20:55 +0000 (11:20 -0800)
committerRobert Griesemer <gri@golang.org>
Tue, 16 Feb 2010 19:20:55 +0000 (11:20 -0800)
Example use: godoc -path=/home/user1:/home/build/foo -http=:6666
will start a local godoc that maps urls starting with /pkg/user1 or
/pkg/foo to the respective roots specified in the path.

Missing: Handling of overlapping package directories, multiple
packages per directory.

R=rsc
CC=golang-dev
https://golang.org/cl/206078

lib/godoc/error.html [moved from lib/godoc/source.html with 56% similarity]
lib/godoc/godoc.html
lib/godoc/package.html
lib/godoc/search.html
src/cmd/godoc/Makefile
src/cmd/godoc/godoc.go
src/cmd/godoc/main.go
src/cmd/godoc/mapping.go

similarity index 56%
rename from lib/godoc/source.html
rename to lib/godoc/error.html
index 4189f4ef8050012e83d398a5394e45550f9ba7d3..c14c5740576f88fce21e7984ae364896c7491bce 100644 (file)
@@ -4,10 +4,6 @@
        license that can be found in the LICENSE file.
 -->
 
-{.section Error}
-       <p>
-       <span class="alert" style="font-size:120%">{@|html}</span>
-       </p>
-{.or}
-       <pre>{Source|html}</pre>
-{.end}
+<p>
+<span class="alert" style="font-size:120%">{@|html-esc}</span>
+</p>
index 8939825007cbcc2fdc112f8da2f734005d4dccc0..944643a34f5be8529be14f1ba4d544a008a71642 100644 (file)
@@ -94,6 +94,9 @@
     <li><a href="/doc/code.html">How to write code</a></li>
     <li><a href="/cmd">Command documentation</a></li>
     <li><a href="/pkg">Package documentation</a></li>
+    {.repeated section PkgRoots}
+    <li><a href="/pkg/{@|html-esc}">Package documentation for {@|html-esc}</a></li>
+    {.end}
     <li><a href="/src">Source files</a></li>
     <li><a href="/doc/devel/">The Go project</a></li>
 
index 6a799c2fd8da10a627304de07ec35fd945613218..2113e46bd8a56efd0f61c644339b984ea005ad62 100644 (file)
@@ -16,7 +16,7 @@
                        <h4>Package files</h4>
                        <span style="font-size:90%">
                        {.repeated section @}
-                               <a href="/{FilePath|html-esc}/{@|html-esc}">{@|html}</a>
+                               <a href="{@|url-src}">{@|localname}</a>
                        {.end}
                        </span>
                        </p>
        {.end}
        {.section Funcs}
                {.repeated section @}
-                       <h2 id="{Name|html-esc}">func <a href="{Decl|link}">{Name|html}</a></h2>
+                       <h2 id="{Name|html-esc}">func <a href="{Decl|url-pos}">{Name|html}</a></h2>
                        <p><code>{Decl|html}</code></p>
                        {Doc|html-comment}
                {.end}
        {.end}
        {.section Types}
                {.repeated section @}
-                       <h2 id="{Type.Name|html-esc}">type <a href="{Decl|link}">{Type.Name|html}</a></h2>
+                       <h2 id="{Type.Name|html-esc}">type <a href="{Decl|url-pos}">{Type.Name|html}</a></h2>
                        {Doc|html-comment}
                        <p><pre>{Decl|html}</pre></p>
                        {.repeated section Consts}
                                <pre>{Decl|html}</pre>
                        {.end}
                        {.repeated section Factories}
-                               <h3 id="{Type.Name|html-esc}.{Name|html-esc}">func <a href="{Decl|link}">{Name|html}</a></h3>
+                               <h3 id="{Type.Name|html-esc}.{Name|html-esc}">func <a href="{Decl|url-pos}">{Name|html}</a></h3>
                                <p><code>{Decl|html}</code></p>
                                {Doc|html-comment}
                        {.end}
                        {.repeated section Methods}
-                               <h3 id="{Type.Name|html-esc}.{Name|html-esc}">func ({Recv|html}) <a href="{Decl|link}">{Name|html}</a></h3>
+                               <h3 id="{Type.Name|html-esc}.{Name|html-esc}">func ({Recv|html}) <a href="{Decl|url-pos}">{Name|html}</a></h3>
                                <p><code>{Decl|html}</code></p>
                                {Doc|html-comment}
                        {.end}
index 254f9b66f3a2ed7ca5f20f899050f24c41c9797a..a6b7fe29cd6217a3fae6ae14835ee242280f9ccb 100644 (file)
        {.section Decls}
                <h2 id="Global">Package-level declarations</h2>
                {.repeated section @}
-                       <h3 id="Global_{Pak.Path|path}">package <a href="{Pak.Path|path}">{Pak.Name|html}</a></h3>
+                       <h3 id="Global_{Pak.Path|url-pkg}">package <a href="{Pak.Path|url-pkg}">{Pak.Name|html}</a></h3>
                        {.repeated section Files}
                                {.repeated section Groups}
                                        {.repeated section Infos}
-                                               <a href="{File.Path|html-esc}?h={Query|html-esc}#L{@|infoLine}">{File.Path|html}:{@|infoLine}</a>
+                                               <a href="{File.Path|url-src}?h={Query|html-esc}#L{@|infoLine}">{File.Path|html}:{@|infoLine}</a>
                                                <pre>{@|infoSnippet}</pre>
                                        {.end}
                                {.end}
@@ -36,9 +36,9 @@
        {.section Others}
                <h2 id="Local">Local declarations and uses</h2>
                {.repeated section @}
-                       <h3 id="Local_{Pak.Path|path}">package <a href="{Pak.Path|path}">{Pak.Name|html}</a></h3>
+                       <h3 id="Local_{Pak.Path|url-pkg}">package <a href="{Pak.Path|url-pkg}">{Pak.Name|html}</a></h3>
                        {.repeated section Files}
-                               <a href="{File.Path|html-esc}?h={Query|html-esc}">{File.Path|html}</a>
+                               <a href="{File.Path|url-src}?h={Query|html-esc}">{File.Path|html}</a>
                                <table class="layout">
                                {.repeated section Groups}
                                        <tr>
@@ -47,7 +47,7 @@
                                        <td align="left" width="4"></td>
                                        <td>
                                        {.repeated section Infos}
-                                               <a href="{File.Path|html-esc}?h={Query|html-esc}#L{@|infoLine}">{@|infoLine}</a>
+                                               <a href="{File.Path|url-src}?h={Query|html-esc}#L{@|infoLine}">{@|infoLine}</a>
                                        {.end}
                                        </td>
                                        </tr>
index dcd4e5b4e0241c8edc6ab4f9a0cd12ec8d790c87..8928221f0910d83720bfe3ac1e4fdf98413f5753 100644 (file)
@@ -9,6 +9,7 @@ GOFILES=\
        godoc.go\
        index.go\
        main.go\
+       mapping.go\
        snippet.go\
        spec.go\
 
index 97ed329fe25f1586b402d2cd9be39a9630545fbd..9cc194435e5a0298ac11820c957034f11d870ff4 100644 (file)
@@ -77,26 +77,54 @@ func (dt *delayTime) backoff(max int) {
 var (
        verbose = flag.Bool("v", false, "verbose mode")
 
-       // file system roots
+       // "fixed" file system roots
        goroot   string
-       cmdroot  = flag.String("cmdroot", "src/cmd", "root command source directory (if unrooted, relative to goroot)")
-       pkgroot  = flag.String("pkgroot", "src/pkg", "root package source directory (if unrooted, relative to goroot)")
-       tmplroot = flag.String("tmplroot", "lib/godoc", "root template directory (if unrooted, relative to goroot)")
+       cmdroot  string
+       pkgroot  string
+       tmplroot string
+
+       // additional file system roots to consider
+       path = flag.String("path", "", "additional pkg directories")
 
        // layout control
        tabwidth = flag.Int("tabwidth", 4, "tab width")
-)
 
+       // file system mapping
+       fsMap  Mapping // user-defined mapping
+       fsTree RWValue // *Directory tree of packages, updated with each sync
 
-var fsTree RWValue // *Directory tree of packages, updated with each sync
+       // http handlers
+       fileServer http.Handler // default file server
+       cmdHandler httpHandler
+       pkgHandler httpHandler
+)
 
 
-func init() {
+func initRoots() {
        goroot = os.Getenv("GOROOT")
        if goroot == "" {
                goroot = pathutil.Join(os.Getenv("HOME"), "go")
        }
        flag.StringVar(&goroot, "goroot", goroot, "Go root directory")
+
+       // other flags/variables that depend on goroot
+       flag.StringVar(&cmdroot, "cmdroot", pathutil.Join(goroot, "src/cmd"), "command source directory")
+       flag.StringVar(&pkgroot, "pkgroot", pathutil.Join(goroot, "src/pkg"), "package source directory")
+       flag.StringVar(&tmplroot, "tmplroot", pathutil.Join(goroot, "lib/godoc"), "template directory")
+
+       fsMap.Init(*path)
+       fileServer = http.FileServer(goroot, "")
+
+       cmdHandler = httpHandler{"/cmd/", cmdroot, false}
+       pkgHandler = httpHandler{"/pkg/", pkgroot, true}
+}
+
+
+func registerPublicHandlers(mux *http.ServeMux) {
+       mux.Handle(cmdHandler.pattern, &cmdHandler)
+       mux.Handle(pkgHandler.pattern, &pkgHandler)
+       mux.Handle("/search", http.HandlerFunc(search))
+       mux.Handle("/", http.HandlerFunc(serveFile))
 }
 
 
@@ -173,6 +201,31 @@ func firstSentence(s string) string {
 }
 
 
+func absolutePath(path, defaultRoot string) string {
+       abspath := fsMap.ToAbsolute(path)
+       if abspath == "" {
+               // no user-defined mapping found; use default mapping
+               abspath = pathutil.Join(defaultRoot, path)
+       }
+       return abspath
+}
+
+
+func relativePath(path string) string {
+       relpath := fsMap.ToRelative(path)
+       if relpath == "" && strings.HasPrefix(path, goroot+"/") {
+               // no user-defined mapping found; use default mapping
+               relpath = path[len(goroot)+1:]
+       }
+       // 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
+}
+
+
 // ----------------------------------------------------------------------------
 // Package directories
 
@@ -287,26 +340,32 @@ func (dir *Directory) iter(skipRoot bool) <-chan *Directory {
 }
 
 
-// lookup looks for the *Directory for a given path, relative to dir.
-func (dir *Directory) lookup(path string) *Directory {
-       path = pathutil.Clean(path) // no trailing '/'
-
-       if dir == nil || path == "" || path == "." {
-               return dir
+func (dir *Directory) lookupLocal(name string) *Directory {
+       for _, d := range dir.Dirs {
+               if d.Name == name {
+                       return d
+               }
        }
+       return nil
+}
 
-       dpath, dname := pathutil.Split(path)
-       if dpath == "" {
-               // directory-local name
-               for _, d := range dir.Dirs {
-                       if dname == d.Name {
-                               return d
-                       }
+
+// lookup looks for the *Directory for a given path, relative to dir.
+func (dir *Directory) lookup(path string) *Directory {
+       d := strings.Split(dir.Path, "/", 0)
+       p := strings.Split(path, "/", 0)
+       i := 0
+       for i < len(d) {
+               if i >= len(p) || d[i] != p[i] {
+                       return nil
                }
-               return nil
+               i++
        }
-
-       return dir.lookup(dpath).lookup(dname)
+       for dir != nil && i < len(p) {
+               dir = dir.lookupLocal(p[i])
+               i++
+       }
+       return dir
 }
 
 
@@ -383,20 +442,6 @@ func (root *Directory) listing(skipRoot bool) *DirList {
 }
 
 
-func listing(dirs []*os.Dir) *DirList {
-       list := make([]DirEntry, len(dirs)+1)
-       list[0] = DirEntry{0, 1, "..", "..", ""}
-       for i, d := range dirs {
-               p := &list[i+1]
-               p.Depth = 0
-               p.Height = 1
-               p.Path = d.Name
-               p.Name = d.Name
-       }
-       return &DirList{1, list}
-}
-
-
 // ----------------------------------------------------------------------------
 // HTML formatting support
 
@@ -552,12 +597,6 @@ func writeText(w io.Writer, text []byte, html bool) {
 }
 
 
-type StyledNode struct {
-       node   interface{}
-       styler printer.Styler
-}
-
-
 // Write anything to w; optionally html-escaped.
 func writeAny(w io.Writer, x interface{}, html bool) {
        switch v := x.(type) {
@@ -567,8 +606,6 @@ func writeAny(w io.Writer, x interface{}, html bool) {
                writeText(w, strings.Bytes(v), html)
        case ast.Decl, ast.Expr, ast.Stmt, *ast.File:
                writeNode(w, x, html, &defaultStyler)
-       case StyledNode:
-               writeNode(w, v.node, html, v.styler)
        default:
                if html {
                        var buf bytes.Buffer
@@ -609,36 +646,51 @@ func textFmt(w io.Writer, x interface{}, format string) {
 }
 
 
-func removePrefix(s, prefix string) string {
-       if strings.HasPrefix(s, prefix) {
-               return s[len(prefix):]
-       }
-       return s
-}
-
-
-// Template formatter for "path" format.
-func pathFmt(w io.Writer, x interface{}, format string) {
-       // TODO(gri): Need to find a better solution for this.
-       //            This will not work correctly if *cmdroot
-       //            or *pkgroot change.
-       writeAny(w, removePrefix(x.(string), "src"), true)
-}
-
+// Template formatter for the various "url-xxx" formats.
+func urlFmt(w io.Writer, x interface{}, format string) {
+       var path string
+       var line int
 
-// Template formatter for "link" format.
-func linkFmt(w io.Writer, x interface{}, format string) {
-       type Positioner interface {
+       // determine path and position info, if any
+       type positioner interface {
                Pos() token.Position
        }
-       if node, ok := x.(Positioner); ok {
-               pos := node.Pos()
+       switch t := x.(type) {
+       case string:
+               path = t
+       case positioner:
+               pos := t.Pos()
                if pos.IsValid() {
-                       // line id's in html-printed source are of the
-                       // form "L%d" where %d stands for the line number
-                       fmt.Fprintf(w, "/%s#L%d", htmlEscape(pos.Filename), pos.Line)
+                       path = pos.Filename
+                       line = pos.Line
                }
        }
+
+       // map path
+       relpath := relativePath(path)
+
+       // convert to URL
+       switch format {
+       default:
+               // we should never reach here, but be resilient
+               // and assume the url-pkg format instead
+               log.Stderrf("INTERNAL ERROR: urlFmt(%s)", format)
+               fallthrough
+       case "url-pkg":
+               // because of the irregular mapping under goroot
+               // we need to correct certain relative paths
+               if strings.HasPrefix(relpath, "src/pkg/") {
+                       relpath = relpath[len("src/pkg/"):]
+               }
+               template.HTMLEscape(w, strings.Bytes(pkgHandler.pattern+relpath))
+       case "url-src":
+               template.HTMLEscape(w, strings.Bytes("/"+relpath))
+       case "url-pos":
+               // line id's in html-printed source are of the
+               // form "L%d" where %d stands for the line number
+               template.HTMLEscape(w, strings.Bytes("/"+relpath))
+               fmt.Fprintf(w, "#L%d", line)
+       }
 }
 
 
@@ -710,24 +762,33 @@ func dirslashFmt(w io.Writer, x interface{}, format string) {
 }
 
 
+// Template formatter for "localname" format.
+func localnameFmt(w io.Writer, x interface{}, format string) {
+       _, localname := pathutil.Split(x.(string))
+       template.HTMLEscape(w, strings.Bytes(localname))
+}
+
+
 var fmap = template.FormatterMap{
        "": textFmt,
        "html": htmlFmt,
        "html-esc": htmlEscFmt,
        "html-comment": htmlCommentFmt,
-       "path": pathFmt,
-       "link": linkFmt,
+       "url-pkg": urlFmt,
+       "url-src": urlFmt,
+       "url-pos": urlFmt,
        "infoKind": infoKindFmt,
        "infoLine": infoLineFmt,
        "infoSnippet": infoSnippetFmt,
        "padding": paddingFmt,
        "time": timeFmt,
        "dir/": dirslashFmt,
+       "localname": localnameFmt,
 }
 
 
 func readTemplate(name string) *template.Template {
-       path := pathutil.Join(*tmplroot, name)
+       path := pathutil.Join(tmplroot, name)
        data, err := ioutil.ReadFile(path)
        if err != nil {
                log.Exitf("ReadFile %s: %v", path, err)
@@ -742,22 +803,21 @@ func readTemplate(name string) *template.Template {
 
 var (
        dirlistHTML,
+               errorHTML,
                godocHTML,
                packageHTML,
                packageText,
-               searchHTML,
-               sourceHTML *template.Template
+               searchHTML *template.Template
 )
 
 func readTemplates() {
-       // have to delay until after flags processing,
-       // so that main has chdir'ed to goroot.
+       // have to delay until after flags processing, so that tmplroot is known
        dirlistHTML = readTemplate("dirlist.html")
+       errorHTML = readTemplate("error.html")
        godocHTML = readTemplate("godoc.html")
        packageHTML = readTemplate("package.html")
        packageText = readTemplate("package.txt")
        searchHTML = readTemplate("search.html")
-       sourceHTML = readTemplate("source.html")
 }
 
 
@@ -767,6 +827,7 @@ func readTemplates() {
 func servePage(c *http.Conn, title, query string, content []byte) {
        type Data struct {
                Title     string
+               PkgRoots  []string
                Timestamp uint64 // int64 to be compatible with os.Dir.Mtime_ns
                Query     string
                Content   []byte
@@ -775,6 +836,7 @@ func servePage(c *http.Conn, title, query string, content []byte) {
        _, ts := fsTree.get()
        d := Data{
                Title: title,
+               PkgRoots: fsMap.PrefixList(),
                Timestamp: uint64(ts) * 1e9, // timestamp in ns
                Query: query,
                Content: content,
@@ -787,7 +849,7 @@ func servePage(c *http.Conn, title, query string, content []byte) {
 
 
 func serveText(c *http.Conn, text []byte) {
-       c.SetHeader("content-type", "text/plain; charset=utf-8")
+       c.SetHeader("Content-Type", "text/plain; charset=utf-8")
        c.Write(text)
 }
 
@@ -811,12 +873,18 @@ func commentText(src []byte) (text string) {
 }
 
 
-func serveHTMLDoc(c *http.Conn, r *http.Request, path string) {
+func serveError(c *http.Conn, r *http.Request, relpath string, err os.Error) {
+       contents := applyTemplate(errorHTML, "errorHTML", err)
+       servePage(c, "File "+relpath, "", contents)
+}
+
+
+func serveHTMLDoc(c *http.Conn, r *http.Request, abspath, relpath string) {
        // get HTML body contents
-       src, err := ioutil.ReadFile(path)
+       src, err := ioutil.ReadFile(abspath)
        if err != nil {
-               log.Stderrf("%v", err)
-               http.NotFound(c, r)
+               log.Stderrf("ioutil.ReadFile: %s", err)
+               serveError(c, r, relpath, err)
                return
        }
 
@@ -828,7 +896,7 @@ func serveHTMLDoc(c *http.Conn, r *http.Request, path string) {
        }
 
        // if it's the language spec, add tags to EBNF productions
-       if strings.HasSuffix(path, "go_spec.html") {
+       if strings.HasSuffix(abspath, "go_spec.html") {
                var buf bytes.Buffer
                linkify(&buf, src)
                src = buf.Bytes()
@@ -839,24 +907,29 @@ func serveHTMLDoc(c *http.Conn, r *http.Request, path string) {
 }
 
 
-func serveGoSource(c *http.Conn, r *http.Request, path string) {
-       var info struct {
-               Source StyledNode
-               Error  string
+func applyTemplate(t *template.Template, name string, data interface{}) []byte {
+       var buf bytes.Buffer
+       if err := t.Execute(data, &buf); err != nil {
+               log.Stderrf("%s.Execute: %s", name, err)
        }
+       return buf.Bytes()
+}
 
-       file, err := parser.ParseFile(path, nil, nil, parser.ParseComments)
-       info.Source = StyledNode{file, &Styler{linetags: true, highlight: r.FormValue("h")}}
+
+func serveGoSource(c *http.Conn, r *http.Request, abspath, relpath string) {
+       file, err := parser.ParseFile(abspath, nil, nil, parser.ParseComments)
        if err != nil {
-               info.Error = err.String()
+               log.Stderrf("parser.ParseFile: %s", err)
+               serveError(c, r, relpath, err)
+               return
        }
 
        var buf bytes.Buffer
-       if err := sourceHTML.Execute(info, &buf); err != nil {
-               log.Stderrf("sourceHTML.Execute: %s", err)
-       }
+       fmt.Fprintln(&buf, "<pre>")
+       writeNode(&buf, file, true, &Styler{linetags: true, highlight: r.FormValue("h")})
+       fmt.Fprintln(&buf, "</pre>")
 
-       servePage(c, "Source file "+path, "", buf.Bytes())
+       servePage(c, "Source file "+relpath, "", buf.Bytes())
 }
 
 
@@ -916,10 +989,12 @@ func isTextFile(path string) bool {
 }
 
 
-func serveTextFile(c *http.Conn, r *http.Request, path string) {
-       src, err := ioutil.ReadFile(path)
+func serveTextFile(c *http.Conn, r *http.Request, abspath, relpath string) {
+       src, err := ioutil.ReadFile(abspath)
        if err != nil {
-               log.Stderrf("serveTextFile: %s", err)
+               log.Stderrf("ioutil.ReadFile: %s", err)
+               serveError(c, r, relpath, err)
+               return
        }
 
        var buf bytes.Buffer
@@ -927,18 +1002,19 @@ func serveTextFile(c *http.Conn, r *http.Request, path string) {
        template.HTMLEscape(&buf, src)
        fmt.Fprintln(&buf, "</pre>")
 
-       servePage(c, "Text file "+path, "", buf.Bytes())
+       servePage(c, "Text file "+relpath, "", buf.Bytes())
 }
 
 
-func serveDirectory(c *http.Conn, r *http.Request, path string) {
+func serveDirectory(c *http.Conn, r *http.Request, abspath, relpath string) {
        if redirect(c, r) {
                return
        }
 
-       list, err := ioutil.ReadDir(path)
+       list, err := ioutil.ReadDir(abspath)
        if err != nil {
-               http.NotFound(c, r)
+               log.Stderrf("ioutil.ReadDir: %s", err)
+               serveError(c, r, relpath, err)
                return
        }
 
@@ -948,49 +1024,47 @@ func serveDirectory(c *http.Conn, r *http.Request, path string) {
                }
        }
 
-       var buf bytes.Buffer
-       if err := dirlistHTML.Execute(list, &buf); err != nil {
-               log.Stderrf("dirlistHTML.Execute: %s", err)
-       }
-
-       servePage(c, "Directory "+path, "", buf.Bytes())
+       contents := applyTemplate(dirlistHTML, "dirlistHTML", list)
+       servePage(c, "Directory "+relpath, "", contents)
 }
 
 
-var fileServer = http.FileServer(".", "")
-
 func serveFile(c *http.Conn, r *http.Request) {
-       path := pathutil.Join(".", r.URL.Path)
+       relpath := r.URL.Path[1:] // serveFile URL paths start with '/'
+       abspath := absolutePath(relpath, goroot)
 
        // pick off special cases and hand the rest to the standard file server
-       switch ext := pathutil.Ext(path); {
-       case r.URL.Path == "/":
-               serveHTMLDoc(c, r, "doc/root.html")
+       switch r.URL.Path {
+       case "/":
+               serveHTMLDoc(c, r, pathutil.Join(goroot, "doc/root.html"), "doc/root.html")
                return
 
-       case r.URL.Path == "/doc/root.html":
+       case "/doc/root.html":
                // hide landing page from its real name
-               http.NotFound(c, r)
+               http.Redirect(c, "/", http.StatusMovedPermanently)
                return
+       }
 
-       case ext == ".html":
-               if strings.HasSuffix(path, "/index.html") {
+       switch pathutil.Ext(abspath) {
+       case ".html":
+               if strings.HasSuffix(abspath, "/index.html") {
                        // We'll show index.html for the directory.
                        // Use the dir/ version as canonical instead of dir/index.html.
                        http.Redirect(c, r.URL.Path[0:len(r.URL.Path)-len("index.html")], http.StatusMovedPermanently)
                        return
                }
-               serveHTMLDoc(c, r, path)
+               serveHTMLDoc(c, r, abspath, relpath)
                return
 
-       case ext == ".go":
-               serveGoSource(c, r, path)
+       case ".go":
+               serveGoSource(c, r, abspath, relpath)
                return
        }
 
-       dir, err := os.Lstat(path)
+       dir, err := os.Lstat(abspath)
        if err != nil {
-               http.NotFound(c, r)
+               log.Stderr(err)
+               serveError(c, r, abspath, err)
                return
        }
 
@@ -998,16 +1072,16 @@ func serveFile(c *http.Conn, r *http.Request) {
                if redirect(c, r) {
                        return
                }
-               if index := path + "/index.html"; isTextFile(index) {
-                       serveHTMLDoc(c, r, index)
+               if index := abspath + "/index.html"; isTextFile(index) {
+                       serveHTMLDoc(c, r, index, relativePath(index))
                        return
                }
-               serveDirectory(c, r, path)
+               serveDirectory(c, r, abspath, relpath)
                return
        }
 
-       if isTextFile(path) {
-               serveTextFile(c, r, path)
+       if isTextFile(abspath) {
+               serveTextFile(c, r, abspath, relpath)
                return
        }
 
@@ -1018,14 +1092,16 @@ func serveFile(c *http.Conn, r *http.Request) {
 // ----------------------------------------------------------------------------
 // Packages
 
-// Package name used for commands that have non-identifier names.
+// Fake package file and name for commands. Contains the command documentation.
+const fakePkgFile = "doc.go"
 const fakePkgName = "documentation"
 
 
 type PageInfo struct {
-       PDoc  *doc.PackageDoc // nil if no package found
-       Dirs  *DirList        // nil if no directory information found
-       IsPkg bool            // false if this is not documenting a real package
+       Dirname string          // directory containing the package
+       PDoc    *doc.PackageDoc // nil if no package found
+       Dirs    *DirList        // nil if no directory information found
+       IsPkg   bool            // false if this is not documenting a real package
 }
 
 
@@ -1036,62 +1112,22 @@ type httpHandler struct {
 }
 
 
-// getPageInfo returns the PageInfo for a package directory path. If the
-// parameter try is true, no errors are logged if getPageInfo fails.
-// If there is no corresponding package in the directory,
-// PageInfo.PDoc is nil. If there are no subdirectories,
-// PageInfo.Dirs is nil.
+// getPageInfo returns the PageInfo for a package directory path. If
+// the parameter try is true, no errors are logged if getPageInfo fails.
+// If there is no corresponding package in the directory, PageInfo.PDoc
+// is nil. If there are no subdirectories, PageInfo.Dirs is nil.
 //
-func (h *httpHandler) getPageInfo(path string, try bool) PageInfo {
-       var dirname string
-       // If the path starts with a slash or ., ignore $GOROOT.
-       // It would be nice to handle "./dir" too, but godoc chdirs to $GOROOT. TODO: fix.
-       if len(path) > 0 && path[0] == '/' {
-               dirname = path
-               // --- Start of hack
-       } else if len(path) > 0 && path[0] == '.' && workingDir != "" {
-               path = pathutil.Join(workingDir, path)
-               dirname = path
-               // --- End of hack
-       } else {
-               // the path is relative to h.fsroot
-               dirname = pathutil.Join(h.fsRoot, path)
-       }
-
-       // the package name is the directory name within its parent
-       // (use dirname instead of path because dirname is clean; i.e. has no trailing '/')
-       _, pkgname := pathutil.Split(dirname)
+func (h *httpHandler) getPageInfo(relpath string, try bool) PageInfo {
+       dirname := absolutePath(relpath, h.fsRoot)
 
        // filter function to select the desired .go files
        filter := func(d *os.Dir) bool {
-               if isPkgFile(d) {
-
-                       // --- Start of hack.
-                       // An ugly special case: If the path is rooted, just say
-                       // yes in the hope we'll get some output from a directory
-                       // outside $GOROOT.  Ugly but effective for command-line
-                       // output but may not find everything if there are multiple
-                       // packages in the directory, since godoc assumes one
-                       // package per directory.
-                       // TODO: Do this better.
-                       if len(path) > 0 && path[0] == '/' {
-                               return true
-                       }
-                       // --- End of hack.
-
-                       // Some directories contain main packages: Only accept
-                       // files that belong to the expected package so that
-                       // parser.ParsePackage doesn't return "multiple packages
-                       // found" errors.
-                       // Additionally, accept the special package name
-                       // fakePkgName if we are looking at cmd documentation.
-                       name := pkgName(dirname + "/" + d.Name)
-                       return name == pkgname || h.fsRoot == *cmdroot && name == fakePkgName
-               }
-               return false
+               // If we are looking at cmd documentation, only accept
+               // the special fakePkgFile containing the documentation.
+               return isPkgFile(d) && (h.isPkg || d.Name == fakePkgFile)
        }
 
-       // get package AST
+       // get package ASTs
        pkgs, err := parser.ParseDir(dirname, filter, parser.ParseComments)
        if err != nil && !try {
                // TODO: errors should be shown instead of an empty directory
@@ -1101,16 +1137,29 @@ func (h *httpHandler) getPageInfo(path string, try bool) PageInfo {
                // TODO: should handle multiple packages
                log.Stderrf("parser.parseDir: found %d packages", len(pkgs))
        }
+
+       // Get the best matching package: either the first one, or the
+       // first one whose package name matches the directory name.
+       // The package name is the directory name within its parent
+       // (use dirname instead of path because dirname is clean; i.e.
+       // has no trailing '/').
+       _, pkgname := pathutil.Split(dirname)
        var pkg *ast.Package
-       for _, pkg = range pkgs {
-               break // take the first package found
+       for _, p := range pkgs {
+               switch {
+               case pkg == nil:
+                       pkg = p
+               case p.Name == pkgname:
+                       pkg = p
+                       break
+               }
        }
 
        // compute package documentation
        var pdoc *doc.PackageDoc
        if pkg != nil {
                ast.PackageExports(pkg)
-               pdoc = doc.NewPackageDoc(pkg, pathutil.Clean(path)) // no trailing '/' in importpath
+               pdoc = doc.NewPackageDoc(pkg, pathutil.Clean(relpath)) // no trailing '/' in importpath
        }
 
        // get directory information
@@ -1118,15 +1167,18 @@ func (h *httpHandler) getPageInfo(path string, try bool) PageInfo {
        if tree, _ := fsTree.get(); tree != 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 beet computed)
+               // new directory tree has not yet been computed)
+               // TODO(gri) Need to build directory tree for fsMap entries
                dir = tree.(*Directory).lookup(dirname)
-       } else {
+       }
+       if dir == nil {
                // no directory tree present (either early after startup
-               // or command-line mode); compute one level for this page
+               // or command-line mode, or we don't build a tree for the
+               // directory; e.g. google3); compute one level for this page
                dir = newDirectory(dirname, 1)
        }
 
-       return PageInfo{pdoc, dir.listing(true), h.isPkg}
+       return PageInfo{dirname, pdoc, dir.listing(true), h.isPkg}
 }
 
 
@@ -1135,41 +1187,33 @@ func (h *httpHandler) ServeHTTP(c *http.Conn, r *http.Request) {
                return
        }
 
-       path := r.URL.Path
-       path = path[len(h.pattern):]
-       info := h.getPageInfo(path, false)
+       relpath := r.URL.Path[len(h.pattern):]
+       info := h.getPageInfo(relpath, false)
 
-       var buf bytes.Buffer
        if r.FormValue("f") == "text" {
-               if err := packageText.Execute(info, &buf); err != nil {
-                       log.Stderrf("packageText.Execute: %s", err)
-               }
-               serveText(c, buf.Bytes())
+               contents := applyTemplate(packageText, "packageText", info)
+               serveText(c, contents)
                return
        }
 
-       if err := packageHTML.Execute(info, &buf); err != nil {
-               log.Stderrf("packageHTML.Execute: %s", err)
-       }
-
-       if path == "" {
-               path = "." // don't display an empty path
-       }
-       title := "Directory " + path
+       var title string
        if info.PDoc != nil {
                switch {
                case h.isPkg:
                        title = "Package " + info.PDoc.PackageName
                case info.PDoc.PackageName == fakePkgName:
                        // assume that the directory name is the command name
-                       _, pkgname := pathutil.Split(pathutil.Clean(path))
+                       _, pkgname := pathutil.Split(pathutil.Clean(relpath))
                        title = "Command " + pkgname
                default:
                        title = "Command " + info.PDoc.PackageName
                }
+       } else {
+               title = "Directory " + relativePath(info.Dirname)
        }
 
-       servePage(c, title, "", buf.Bytes())
+       contents := applyTemplate(packageHTML, "packageHTML", info)
+       servePage(c, title, "", contents)
 }
 
 
@@ -1197,11 +1241,6 @@ func search(c *http.Conn, r *http.Request) {
                result.Accurate = timestamp >= ts
        }
 
-       var buf bytes.Buffer
-       if err := searchHTML.Execute(result, &buf); err != nil {
-               log.Stderrf("searchHTML.Execute: %s", err)
-       }
-
        var title string
        if result.Hit != nil {
                title = fmt.Sprintf(`Results for query %q`, query)
@@ -1209,28 +1248,14 @@ func search(c *http.Conn, r *http.Request) {
                title = fmt.Sprintf(`No results found for query %q`, query)
        }
 
-       servePage(c, title, query, buf.Bytes())
+       contents := applyTemplate(searchHTML, "searchHTML", result)
+       servePage(c, title, query, contents)
 }
 
 
 // ----------------------------------------------------------------------------
-// Server
-
-var (
-       cmdHandler = httpHandler{"/cmd/", *cmdroot, false}
-       pkgHandler = httpHandler{"/pkg/", *pkgroot, true}
-)
-
-
-func registerPublicHandlers(mux *http.ServeMux) {
-       mux.Handle(cmdHandler.pattern, &cmdHandler)
-       mux.Handle(pkgHandler.pattern, &pkgHandler)
-       mux.Handle("/search", http.HandlerFunc(search))
-       mux.Handle("/", http.HandlerFunc(serveFile))
-}
-
+// Indexer
 
-// Indexing goroutine.
 func indexer() {
        for {
                _, ts := fsTree.get()
@@ -1240,7 +1265,7 @@ func indexer() {
                        // from the sync goroutine, but this solution is
                        // more decoupled, trivial, and works well enough)
                        start := time.Nanoseconds()
-                       index := NewIndex(".")
+                       index := NewIndex(goroot)
                        stop := time.Nanoseconds()
                        searchIndex.set(index)
                        if *verbose {
index f475d8fa1026efd3d0f60f6e3e4d69bf57fb837d..ef08551ce7ee4d13ad595e498f590524a2b45d00 100644 (file)
@@ -47,9 +47,6 @@ var (
 
        // layout control
        html = flag.Bool("html", false, "print HTML in command-line mode")
-
-       // --- Hack to remember current directory
-       workingDir string
 )
 
 
@@ -112,7 +109,7 @@ func dosync(c *http.Conn, r *http.Request) {
                // TODO(gri): The directory tree may be temporarily out-of-sync.
                //            Consider keeping separate time stamps so the web-
                //            page can indicate this discrepancy.
-               fsTree.set(newDirectory(".", maxDirDepth))
+               fsTree.set(newDirectory(goroot, maxDirDepth))
                fallthrough
        case 1:
                // sync failed because no files changed;
@@ -155,17 +152,7 @@ func main() {
                log.Exitf("negative tabwidth %d", *tabwidth)
        }
 
-       // ---  Start of hack.
-       // Remember where we were, so "." works as a directory name.
-       // Error's not worth worrying about; we just check for empty string
-       // when we need it.
-       workingDir, _ = os.Getwd()
-       // --- End of hack.
-
-       if err := os.Chdir(goroot); err != nil {
-               log.Exitf("chdir %s: %v", goroot, err)
-       }
-
+       initRoots()
        readTemplates()
 
        if *httpaddr != "" {
@@ -175,10 +162,14 @@ func main() {
                        log.Stderrf("Go Documentation Server\n")
                        log.Stderrf("address = %s\n", *httpaddr)
                        log.Stderrf("goroot = %s\n", goroot)
-                       log.Stderrf("cmdroot = %s\n", *cmdroot)
-                       log.Stderrf("pkgroot = %s\n", *pkgroot)
-                       log.Stderrf("tmplroot = %s\n", *tmplroot)
+                       log.Stderrf("cmdroot = %s\n", cmdroot)
+                       log.Stderrf("pkgroot = %s\n", pkgroot)
+                       log.Stderrf("tmplroot = %s\n", tmplroot)
                        log.Stderrf("tabwidth = %d\n", *tabwidth)
+                       if !fsMap.IsEmpty() {
+                               log.Stderr("user-defined mapping:")
+                               fsMap.Fprint(os.Stderr)
+                       }
                        handler = loggingHandler(handler)
                }
 
@@ -192,7 +183,7 @@ func main() {
                // 1) set timestamp right away so that the indexer is kicked on
                fsTree.set(nil)
                // 2) compute initial directory tree in a goroutine so that launch is quick
-               go func() { fsTree.set(newDirectory(".", maxDirDepth)) }()
+               go func() { fsTree.set(newDirectory(goroot, maxDirDepth)) }()
 
                // Start sync goroutine, if enabled.
                if *syncCmd != "" && *syncMin > 0 {
index 1143a4bdc005924fb07a58fa504cf32f206e7756..ed2483d8be481e53e49109c7127a760849e20d8c 100644 (file)
@@ -27,13 +27,13 @@ import (
 // until a valid mapping is found. For instance, for the mapping:
 //
 //     user   -> /home/user
-//      public -> /home/user/public
+//     public -> /home/user/public
 //     public -> /home/build/public
 //
 // the relative paths below are mapped to absolute paths as follows:
 //
 //     user/foo                -> /home/user/foo
-//      public/net/rpc/file1.go -> /home/user/public/net/rpc/file1.go
+//     public/net/rpc/file1.go -> /home/user/public/net/rpc/file1.go
 //
 // If there is no /home/user/public/net/rpc/file2.go, the next public
 // mapping entry is used to map the relative path to:
@@ -43,7 +43,8 @@ import (
 // (assuming that file exists).
 //
 type Mapping struct {
-       list []mapping
+       list     []mapping
+       prefixes []string
 }
 
 
@@ -71,7 +72,7 @@ type mapping struct {
 // leads to the following mapping:
 //
 //     user   -> /home/user
-//      public -> /home/build/public
+//     public -> /home/build/public
 //
 func (m *Mapping) Init(paths string) {
        cwd, _ := os.Getwd() // ignore errors
@@ -105,7 +106,7 @@ func (m *Mapping) Init(paths string) {
                // add mapping if it is new
                if i >= n {
                        _, prefix := pathutil.Split(path)
-                       list[i] = mapping{prefix, path}
+                       list[n] = mapping{prefix, path}
                        n++
                }
        }
@@ -118,6 +119,45 @@ func (m *Mapping) Init(paths string) {
 func (m *Mapping) IsEmpty() bool { return len(m.list) == 0 }
 
 
+// PrefixList returns a list of all prefixes, with duplicates removed.
+// For instance, for the mapping:
+//
+//     user   -> /home/user
+//     public -> /home/user/public
+//     public -> /home/build/public
+//
+// the prefix list is:
+//
+//     user, public
+//
+func (m *Mapping) PrefixList() []string {
+       // compute the list lazily
+       if m.prefixes == nil {
+               list := make([]string, len(m.list))
+               n := 0 // nuber of prefixes
+
+               for _, e := range m.list {
+                       // check if prefix exists already
+                       var i int
+                       for i = 0; i < n; i++ {
+                               if e.prefix == list[i] {
+                                       break
+                               }
+                       }
+
+                       // add prefix if it is new
+                       if i >= n {
+                               list[n] = e.prefix
+                               n++
+                       }
+               }
+               m.prefixes = list[0:n]
+       }
+
+       return m.prefixes
+}
+
+
 // Fprint prints the mapping.
 func (m *Mapping) Fprint(w io.Writer) {
        for _, e := range m.list {