From e6ff53bcc832d124109c26270e3e5f4dda8da629 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Wed, 17 Jul 2013 17:09:54 +1000 Subject: [PATCH] godoc: move bulk of the code to the package R=golang-dev, adg CC=golang-dev https://golang.org/cl/11424043 --- cmd/godoc/codewalk.go | 23 +- cmd/godoc/filesystem.go | 48 -- cmd/godoc/godoc.go | 1361 ++---------------------------- cmd/godoc/main.go | 97 +-- cmd/godoc/play.go | 6 + {cmd/godoc => godoc}/dirtrees.go | 8 +- {cmd/godoc => godoc}/format.go | 7 +- {cmd/godoc => godoc}/index.go | 198 +++-- {cmd/godoc => godoc}/linkify.go | 4 +- {cmd/godoc => godoc}/parser.go | 6 +- {cmd/godoc => godoc}/snippet.go | 2 +- {cmd/godoc => godoc}/spec.go | 2 +- {cmd/godoc => godoc}/template.go | 8 +- godoc/vfs/vfs.go | 11 + 14 files changed, 320 insertions(+), 1461 deletions(-) delete mode 100644 cmd/godoc/filesystem.go rename {cmd/godoc => godoc}/dirtrees.go (98%) rename {cmd/godoc => godoc}/format.go (98%) rename {cmd/godoc => godoc}/index.go (88%) rename {cmd/godoc => godoc}/linkify.go (99%) rename {cmd/godoc => godoc}/parser.go (90%) rename {cmd/godoc => godoc}/snippet.go (99%) rename {cmd/godoc => godoc}/spec.go (99%) rename {cmd/godoc => godoc}/template.go (97%) diff --git a/cmd/godoc/codewalk.go b/cmd/godoc/codewalk.go index e68c0fa6ba..c62e0be4cc 100644 --- a/cmd/godoc/codewalk.go +++ b/cmd/godoc/codewalk.go @@ -26,6 +26,9 @@ import ( "strings" "text/template" "unicode/utf8" + + "code.google.com/p/go.tools/godoc" + "code.google.com/p/go.tools/godoc/vfs" ) // Handler for /doc/codewalk/ and below. @@ -40,7 +43,7 @@ func codewalk(w http.ResponseWriter, r *http.Request) { } // If directory exists, serve list of code walks. - dir, err := fs.Lstat(abspath) + dir, err := godoc.FS.Lstat(abspath) if err == nil && dir.IsDir() { codewalkDir(w, r, relpath, abspath) return @@ -59,7 +62,7 @@ func codewalk(w http.ResponseWriter, r *http.Request) { cw, err := loadCodewalk(abspath + ".xml") if err != nil { log.Print(err) - serveError(w, r, relpath, err) + godoc.ServeError(w, r, relpath, err) return } @@ -68,7 +71,7 @@ func codewalk(w http.ResponseWriter, r *http.Request) { return } - servePage(w, Page{ + godoc.ServePage(w, godoc.Page{ Title: "Codewalk: " + cw.Title, Tabtitle: cw.Title, Body: applyTemplate(codewalkHTML, "codewalk", cw), @@ -114,7 +117,7 @@ func (st *Codestep) String() string { // loadCodewalk reads a codewalk from the named XML file. func loadCodewalk(filename string) (*Codewalk, error) { - f, err := fs.Open(filename) + f, err := godoc.FS.Open(filename) if err != nil { return nil, err } @@ -135,7 +138,7 @@ func loadCodewalk(filename string) (*Codewalk, error) { i = len(st.Src) } filename := st.Src[0:i] - data, err := ReadFile(fs, filename) + data, err := vfs.ReadFile(godoc.FS, filename) if err != nil { st.Err = err continue @@ -182,10 +185,10 @@ func codewalkDir(w http.ResponseWriter, r *http.Request, relpath, abspath string Title string } - dir, err := fs.ReadDir(abspath) + dir, err := godoc.FS.ReadDir(abspath) if err != nil { log.Print(err) - serveError(w, r, relpath, err) + godoc.ServeError(w, r, relpath, err) return } var v []interface{} @@ -202,7 +205,7 @@ func codewalkDir(w http.ResponseWriter, r *http.Request, relpath, abspath string } } - servePage(w, Page{ + godoc.ServePage(w, godoc.Page{ Title: "Codewalks", Body: applyTemplate(codewalkdirHTML, "codewalkdir", v), }) @@ -216,10 +219,10 @@ func codewalkDir(w http.ResponseWriter, r *http.Request, relpath, abspath string // the usual godoc HTML wrapper. func codewalkFileprint(w http.ResponseWriter, r *http.Request, f string) { abspath := f - data, err := ReadFile(fs, abspath) + data, err := vfs.ReadFile(godoc.FS, abspath) if err != nil { log.Print(err) - serveError(w, r, f, err) + godoc.ServeError(w, r, f, err) return } lo, _ := strconv.Atoi(r.FormValue("lo")) diff --git a/cmd/godoc/filesystem.go b/cmd/godoc/filesystem.go deleted file mode 100644 index bc54e5b340..0000000000 --- a/cmd/godoc/filesystem.go +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2011 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. - -// This file defines types for abstract file system access and -// provides an implementation accessing the file system of the -// underlying OS. - -package main - -import ( - "io/ioutil" - - "code.google.com/p/go.tools/godoc/vfs" -) - -// fs is the file system that godoc reads from and serves. -// It is a virtual file system that operates on slash-separated paths, -// and its root corresponds to the Go distribution root: /src/pkg -// 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 -// system, which helps keep things simple. -// -// New file trees - implementations of FileSystem - can be added to -// the virtual file system using nameSpace's Bind method. -// The usual setup is to bind OS(runtime.GOROOT) to the root -// of the name space and then bind any GOPATH/src directories -// on top of /src/pkg, so that all sources are in /src/pkg. -// -// For more about name spaces, see the NameSpace type's -// documentation in code.google.com/p/go.tools/godoc/vfs. -// -// The use of this virtual file system means that most code processing -// paths can assume they are slash-separated and should be using -// package path (often imported as pathpkg) to manipulate them, -// even on Windows. -// -var fs = vfs.NameSpace{} // the underlying file system for godoc - -// ReadFile reads the file named by path from fs and returns the contents. -func ReadFile(fs vfs.FileSystem, path string) ([]byte, error) { - rc, err := fs.Open(path) - if err != nil { - return nil, err - } - defer rc.Close() - return ioutil.ReadAll(rc) -} diff --git a/cmd/godoc/godoc.go b/cmd/godoc/godoc.go index 1f40111a6d..ce6377b000 100644 --- a/cmd/godoc/godoc.go +++ b/cmd/godoc/godoc.go @@ -6,40 +6,50 @@ package main import ( "bytes" - "encoding/json" "flag" "fmt" - "go/ast" - "go/build" - "go/doc" - "go/format" - "go/printer" - "go/token" htmlpkg "html" - "io" - "io/ioutil" + "log" "net/http" "net/url" - "os" + pathpkg "path" - "path/filepath" + "regexp" "runtime" - "sort" + "strings" "text/template" - "time" - "unicode" - "unicode/utf8" + "code.google.com/p/go.tools/godoc" "code.google.com/p/go.tools/godoc/util" - "code.google.com/p/go.tools/godoc/vfs/httpfs" + "code.google.com/p/go.tools/godoc/vfs" ) // ---------------------------------------------------------------------------- // Globals +func flagBool(b *bool, name string, value bool, usage string) interface{} { + flag.BoolVar(b, name, value, usage) + return nil +} + +func flagInt(v *int, name string, value int, usage string) interface{} { + flag.IntVar(v, name, value, usage) + return nil +} + +func flagString(v *string, name string, value string, usage string) interface{} { + flag.StringVar(v, name, value, usage) + return nil +} + +func flagFloat64(v *float64, name string, value float64, usage string) interface{} { + flag.Float64Var(v, name, value, usage) + return nil +} + var ( verbose = flag.Bool("v", false, "verbose mode") @@ -49,661 +59,85 @@ var ( testDir = flag.String("testdir", "", "Go root subdirectory - for testing only (faster startups)") // layout control - tabwidth = flag.Int("tabwidth", 4, "tab width") - showTimestamps = flag.Bool("timestamps", false, "show timestamps with directory listings") - templateDir = flag.String("templates", "", "directory containing alternate template files") - showPlayground = flag.Bool("play", false, "enable playground in web interface") - showExamples = flag.Bool("ex", false, "show examples in command line mode") - declLinks = flag.Bool("links", true, "link identifiers to their declarations") + _ = flagInt(&godoc.TabWidth, "tabwidth", 4, "tab width") + _ = flagBool(&godoc.ShowTimestamps, "timestamps", false, "show timestamps with directory listings") + templateDir = flag.String("templates", "", "directory containing alternate template files") + _ = flagBool(&godoc.ShowPlayground, "play", false, "enable playground in web interface") + _ = flagBool(&godoc.ShowExamples, "ex", false, "show examples in command line mode") + _ = flagBool(&godoc.DeclLinks, "links", true, "link identifiers to their declarations") // search index - indexEnabled = flag.Bool("index", false, "enable search index") - indexFiles = flag.String("index_files", "", "glob pattern specifying index files;"+ + _ = flagBool(&godoc.IndexEnabled, "index", false, "enable search index") + _ = flagString(&godoc.IndexFiles, "index_files", "", "glob pattern specifying index files;"+ "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 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 - - // http handlers - fileServer http.Handler // default file server - cmdHandler docServer - pkgHandler docServer + _ = 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") // source code notes - notes = flag.String("notes", "BUG", "regular expression matching note markers to show") + _ = flagString(&godoc.NotesRx, "notes", "BUG", "regular expression matching note markers to show") ) -func initHandlers() { - fileServer = http.FileServer(httpfs.New(fs)) - cmdHandler = docServer{"/cmd/", "/src/cmd"} - pkgHandler = docServer{"/pkg/", "/src/pkg"} -} - func registerPublicHandlers(mux *http.ServeMux) { - mux.Handle(cmdHandler.pattern, &cmdHandler) - mux.Handle(pkgHandler.pattern, &pkgHandler) + godoc.CmdHandler.RegisterWithMux(mux) + godoc.PkgHandler.RegisterWithMux(mux) mux.HandleFunc("/doc/codewalk/", codewalk) - mux.Handle("/doc/play/", fileServer) + mux.Handle("/doc/play/", godoc.FileServer) mux.HandleFunc("/search", search) - mux.Handle("/robots.txt", fileServer) + mux.Handle("/robots.txt", godoc.FileServer) mux.HandleFunc("/opensearch.xml", serveSearchDesc) mux.HandleFunc("/", serveFile) } func initFSTree() { - dir := newDirectory(pathpkg.Join("/", *testDir), -1) + dir := godoc.NewDirectory(pathpkg.Join("/", *testDir), -1) if dir == nil { log.Println("Warning: FSTree is nil") return } - fsTree.Set(dir) - invalidateIndex() -} - -// ---------------------------------------------------------------------------- -// Tab conversion - -var spaces = []byte(" ") // 32 spaces seems like a good number - -const ( - indenting = iota - collecting -) - -// A tconv is an io.Writer filter for converting leading tabs into spaces. -type tconv struct { - output io.Writer - state int // indenting or collecting - indent int // valid if state == indenting -} - -func (p *tconv) writeIndent() (err error) { - i := p.indent - for i >= len(spaces) { - i -= len(spaces) - if _, err = p.output.Write(spaces); err != nil { - return - } - } - // i < len(spaces) - if i > 0 { - _, err = p.output.Write(spaces[0:i]) - } - return -} - -func (p *tconv) Write(data []byte) (n int, err error) { - if len(data) == 0 { - return - } - pos := 0 // valid if p.state == collecting - var b byte - for n, b = range data { - switch p.state { - case indenting: - switch b { - case '\t': - p.indent += *tabwidth - case '\n': - p.indent = 0 - if _, err = p.output.Write(data[n : n+1]); err != nil { - return - } - case ' ': - p.indent++ - default: - p.state = collecting - pos = n - if err = p.writeIndent(); err != nil { - return - } - } - case collecting: - if b == '\n' { - p.state = indenting - p.indent = 0 - if _, err = p.output.Write(data[pos : n+1]); err != nil { - return - } - } - } - } - n = len(data) - if pos < n && p.state == collecting { - _, err = p.output.Write(data[pos:]) - } - return + godoc.FSTree.Set(dir) + godoc.InvalidateIndex() } // ---------------------------------------------------------------------------- // Templates -// Write an AST node to w. -func writeNode(w io.Writer, fset *token.FileSet, x interface{}) { - // convert trailing tabs into spaces using a tconv filter - // to ensure a good outcome in most browsers (there may still - // be tabs in comments and strings, but converting those into - // the right number of spaces is much harder) - // - // TODO(gri) rethink printer flags - perhaps tconv can be eliminated - // with an another printer mode (which is more efficiently - // implemented in the printer than here with another layer) - mode := printer.TabIndent | printer.UseSpaces - err := (&printer.Config{Mode: mode, Tabwidth: *tabwidth}).Fprint(&tconv{output: w}, fset, x) - if err != nil { - log.Print(err) - } -} - -func filenameFunc(path string) string { - _, localname := pathpkg.Split(path) - return localname -} - -func fileInfoNameFunc(fi os.FileInfo) string { - name := fi.Name() - if fi.IsDir() { - name += "/" - } - return name -} - -func fileInfoTimeFunc(fi os.FileInfo) string { - if t := fi.ModTime(); t.Unix() != 0 { - return t.Local().String() - } - return "" // don't return epoch if time is obviously not set -} - -// The strings in infoKinds must be properly html-escaped. -var infoKinds = [nKinds]string{ - PackageClause: "package clause", - ImportDecl: "import decl", - ConstDecl: "const decl", - TypeDecl: "type decl", - VarDecl: "var decl", - FuncDecl: "func decl", - MethodDecl: "method decl", - Use: "use", -} - -func infoKind_htmlFunc(info SpotInfo) string { - return infoKinds[info.Kind()] // infoKind entries are html-escaped -} - -func infoLineFunc(info SpotInfo) int { - line := info.Lori() - if info.IsIndex() { - index, _ := searchIndex.Get() - if index != nil { - line = index.(*Index).Snippet(line).Line - } else { - // no line information available because - // we don't have an index - this should - // never happen; be conservative and don't - // crash - line = 0 - } - } - return line -} - -func infoSnippet_htmlFunc(info SpotInfo) string { - if info.IsIndex() { - index, _ := searchIndex.Get() - // Snippet.Text was HTML-escaped when it was generated - return index.(*Index).Snippet(info.Lori()).Text - } - return `no snippet text available` -} - -func nodeFunc(info *PageInfo, node interface{}) string { - var buf bytes.Buffer - writeNode(&buf, info.FSet, node) - return buf.String() -} - -func node_htmlFunc(info *PageInfo, node interface{}, linkify bool) string { - var buf1 bytes.Buffer - writeNode(&buf1, info.FSet, node) - - var buf2 bytes.Buffer - if n, _ := node.(ast.Node); n != nil && linkify && *declLinks { - LinkifyText(&buf2, buf1.Bytes(), n) - } else { - FormatText(&buf2, buf1.Bytes(), -1, true, "", nil) - } - - return buf2.String() -} - -func comment_htmlFunc(comment string) string { - var buf bytes.Buffer - // TODO(gri) Provide list of words (e.g. function parameters) - // to be emphasized by ToHTML. - doc.ToHTML(&buf, comment, nil) // does html-escaping - return buf.String() -} - -// punchCardWidth is the number of columns of fixed-width -// characters to assume when wrapping text. Very few people -// use terminals or cards smaller than 80 characters, so 80 it is. -// We do not try to sniff the environment or the tty to adapt to -// the situation; instead, by using a constant we make sure that -// godoc always produces the same output regardless of context, -// a consistency that is lost otherwise. For example, if we sniffed -// the environment or tty, then http://golang.org/pkg/math/?m=text -// would depend on the width of the terminal where godoc started, -// which is clearly bogus. More generally, the Unix tools that behave -// differently when writing to a tty than when writing to a file have -// a history of causing confusion (compare `ls` and `ls | cat`), and we -// want to avoid that mistake here. -const punchCardWidth = 80 - -func comment_textFunc(comment, indent, preIndent string) string { - var buf bytes.Buffer - doc.ToText(&buf, comment, indent, preIndent, punchCardWidth-2*len(indent)) - return buf.String() -} - -func startsWithUppercase(s string) bool { - r, _ := utf8.DecodeRuneInString(s) - return unicode.IsUpper(r) -} - -var exampleOutputRx = regexp.MustCompile(`(?i)//[[:space:]]*output:`) - -// stripExampleSuffix strips lowercase braz in Foo_braz or Foo_Bar_braz from name -// while keeping uppercase Braz in Foo_Braz. -func stripExampleSuffix(name string) string { - if i := strings.LastIndex(name, "_"); i != -1 { - if i < len(name)-1 && !startsWithUppercase(name[i+1:]) { - name = name[:i] - } - } - return name -} - -func example_textFunc(info *PageInfo, funcName, indent string) string { - if !*showExamples { - return "" - } - - var buf bytes.Buffer - first := true - for _, eg := range info.Examples { - name := stripExampleSuffix(eg.Name) - if name != funcName { - continue - } - - if !first { - buf.WriteString("\n") - } - first = false - - // print code - cnode := &printer.CommentedNode{Node: eg.Code, Comments: eg.Comments} - var buf1 bytes.Buffer - writeNode(&buf1, info.FSet, cnode) - code := buf1.String() - // Additional formatting if this is a function body. - if n := len(code); n >= 2 && code[0] == '{' && code[n-1] == '}' { - // remove surrounding braces - code = code[1 : n-1] - // unindent - code = strings.Replace(code, "\n ", "\n", -1) - } - code = strings.Trim(code, "\n") - code = strings.Replace(code, "\n", "\n\t", -1) - - buf.WriteString(indent) - buf.WriteString("Example:\n\t") - buf.WriteString(code) - buf.WriteString("\n") - } - return buf.String() -} - -func example_htmlFunc(info *PageInfo, funcName string) string { - var buf bytes.Buffer - for _, eg := range info.Examples { - name := stripExampleSuffix(eg.Name) - - if name != funcName { - continue - } - - // print code - cnode := &printer.CommentedNode{Node: eg.Code, Comments: eg.Comments} - code := node_htmlFunc(info, cnode, true) - out := eg.Output - wholeFile := true - - // Additional formatting if this is a function body. - if n := len(code); n >= 2 && code[0] == '{' && code[n-1] == '}' { - wholeFile = false - // remove surrounding braces - code = code[1 : n-1] - // unindent - code = strings.Replace(code, "\n ", "\n", -1) - // remove output comment - if loc := exampleOutputRx.FindStringIndex(code); loc != nil { - code = strings.TrimSpace(code[:loc[0]]) - } - } - - // Write out the playground code in standard Go style - // (use tabs, no comment highlight, etc). - play := "" - if eg.Play != nil && *showPlayground { - var buf bytes.Buffer - if err := format.Node(&buf, info.FSet, eg.Play); err != nil { - log.Print(err) - } else { - play = buf.String() - } - } - - // Drop output, as the output comment will appear in the code. - if wholeFile && play == "" { - out = "" - } - - err := exampleHTML.Execute(&buf, struct { - Name, Doc, Code, Play, Output string - }{eg.Name, eg.Doc, code, play, out}) - if err != nil { - log.Print(err) - } - } - return buf.String() -} - -// example_nameFunc takes an example function name and returns its display -// name. For example, "Foo_Bar_quux" becomes "Foo.Bar (Quux)". -func example_nameFunc(s string) string { - name, suffix := splitExampleName(s) - // replace _ with . for method names - name = strings.Replace(name, "_", ".", 1) - // use "Package" if no name provided - if name == "" { - name = "Package" - } - return name + suffix -} - -// example_suffixFunc takes an example function name and returns its suffix in -// parenthesized form. For example, "Foo_Bar_quux" becomes " (Quux)". -func example_suffixFunc(name string) string { - _, suffix := splitExampleName(name) - return suffix -} - -func noteTitle(note string) string { - return strings.Title(strings.ToLower(note)) -} - -func splitExampleName(s string) (name, suffix string) { - i := strings.LastIndex(s, "_") - if 0 <= i && i < len(s)-1 && !startsWithUppercase(s[i+1:]) { - name = s[:i] - suffix = " (" + strings.Title(s[i+1:]) + ")" - return - } - name = s - return -} - -func pkgLinkFunc(path string) string { - relpath := path[1:] - // because of the irregular mapping under goroot - // we need to correct certain relative paths - relpath = strings.TrimPrefix(relpath, "src/pkg/") - return pkgHandler.pattern[1:] + relpath // remove trailing '/' for relative URL -} - -// n must be an ast.Node or a *doc.Note -func posLink_urlFunc(info *PageInfo, n interface{}) string { - var pos, end token.Pos - - switch n := n.(type) { - case ast.Node: - pos = n.Pos() - end = n.End() - case *doc.Note: - pos = n.Pos - end = n.End - default: - panic(fmt.Sprintf("wrong type for posLink_url template formatter: %T", n)) - } - - var relpath string - var line int - var low, high int // selection offset range - - if pos.IsValid() { - p := info.FSet.Position(pos) - relpath = p.Filename - line = p.Line - low = p.Offset - } - if end.IsValid() { - high = info.FSet.Position(end).Offset - } - - var buf bytes.Buffer - template.HTMLEscape(&buf, []byte(relpath)) - // selection ranges are of form "s=low:high" - if low < high { - fmt.Fprintf(&buf, "?s=%d:%d", low, high) // no need for URL escaping - // if we have a selection, position the page - // such that the selection is a bit below the top - line -= 10 - if line < 1 { - line = 1 - } - } - // line id's in html-printed source are of the - // form "L%d" where %d stands for the line number - if line > 0 { - fmt.Fprintf(&buf, "#L%d", line) // no need for URL escaping - } - - 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 -// require explicit escaping in the template. -var fmap = template.FuncMap{ - // various helpers - "filename": filenameFunc, - "repeat": strings.Repeat, - - // access to FileInfos (directory listings) - "fileInfoName": fileInfoNameFunc, - "fileInfoTime": fileInfoTimeFunc, - - // access to search result information - "infoKind_html": infoKind_htmlFunc, - "infoLine": infoLineFunc, - "infoSnippet_html": infoSnippet_htmlFunc, - - // formatting of AST nodes - "node": nodeFunc, - "node_html": node_htmlFunc, - "comment_html": comment_htmlFunc, - "comment_text": comment_textFunc, - - // support for URL attributes - "pkgLink": pkgLinkFunc, - "srcLink": srcLinkFunc, - "posLink_url": posLink_urlFunc, - - // formatting of Examples - "example_html": example_htmlFunc, - "example_text": example_textFunc, - "example_name": example_nameFunc, - "example_suffix": example_suffixFunc, - - // formatting of Notes - "noteTitle": noteTitle, -} - func readTemplate(name string) *template.Template { path := "lib/godoc/" + name // use underlying file system fs to read the template file // (cannot use template ParseFile functions directly) - data, err := ReadFile(fs, path) + data, err := vfs.ReadFile(godoc.FS, path) if err != nil { log.Fatal("readTemplate: ", err) } // be explicit with errors (for app engine use) - t, err := template.New(name).Funcs(fmap).Parse(string(data)) + t, err := template.New(name).Funcs(godoc.FuncMap).Parse(string(data)) if err != nil { log.Fatal("readTemplate: ", err) } return t } -var ( - codewalkHTML, - codewalkdirHTML, - dirlistHTML, - errorHTML, - exampleHTML, - godocHTML, - packageHTML, - packageText, - searchHTML, - searchText, - searchDescXML *template.Template -) +var codewalkHTML, codewalkdirHTML *template.Template func readTemplates() { // have to delay until after flags processing since paths depend on goroot codewalkHTML = readTemplate("codewalk.html") codewalkdirHTML = readTemplate("codewalkdir.html") - dirlistHTML = readTemplate("dirlist.html") - errorHTML = readTemplate("error.html") - exampleHTML = readTemplate("example.html") - godocHTML = readTemplate("godoc.html") - packageHTML = readTemplate("package.html") - packageText = readTemplate("package.txt") - searchHTML = readTemplate("search.html") - searchText = readTemplate("search.txt") - searchDescXML = readTemplate("opensearch.xml") -} - -// ---------------------------------------------------------------------------- -// Generic HTML wrapper - -// Page describes the contents of the top-level godoc webpage. -type Page struct { - Title string - Tabtitle string - Subtitle string - Query string - Body []byte - - // filled in by servePage - SearchBox bool - Playground bool - Version string -} - -func servePage(w http.ResponseWriter, page Page) { - if page.Tabtitle == "" { - page.Tabtitle = page.Title - } - page.SearchBox = *indexEnabled - page.Playground = *showPlayground - page.Version = runtime.Version() - if err := godocHTML.Execute(w, page); err != nil && err != http.ErrBodyNotAllowed { - // Only log if there's an error that's not about writing on HEAD requests. - // See Issues 5451 and 5454. - log.Printf("godocHTML.Execute: %s", err) - } -} - -func serveText(w http.ResponseWriter, text []byte) { - w.Header().Set("Content-Type", "text/plain; charset=utf-8") - w.Write(text) + godoc.DirlistHTML = readTemplate("dirlist.html") + godoc.ErrorHTML = readTemplate("error.html") + godoc.ExampleHTML = readTemplate("example.html") + godoc.GodocHTML = readTemplate("godoc.html") + godoc.PackageHTML = readTemplate("package.html") + godoc.PackageText = readTemplate("package.txt") + godoc.SearchHTML = readTemplate("search.html") + godoc.SearchText = readTemplate("search.txt") + godoc.SearchDescXML = readTemplate("opensearch.xml") } // ---------------------------------------------------------------------------- // Files -var ( - doctype = []byte("") -) - -func serveHTMLDoc(w http.ResponseWriter, r *http.Request, abspath, relpath string) { - // get HTML body contents - src, err := ReadFile(fs, abspath) - if err != nil { - log.Printf("ReadFile: %s", err) - serveError(w, r, relpath, err) - return - } - - // if it begins with "") - FormatText(&buf, src, 1, pathpkg.Ext(abspath) == ".go", r.FormValue("h"), rangeSelection(r.FormValue("s"))) + godoc.FormatText(&buf, src, 1, pathpkg.Ext(abspath) == ".go", r.FormValue("h"), godoc.RangeSelection(r.FormValue("s"))) buf.WriteString("") fmt.Fprintf(&buf, `

View as plain text

`, htmlpkg.EscapeString(relpath)) - servePage(w, Page{ + godoc.ServePage(w, godoc.Page{ Title: title + " " + relpath, Tabtitle: relpath, Body: buf.Bytes(), @@ -769,16 +203,16 @@ func serveDirectory(w http.ResponseWriter, r *http.Request, abspath, relpath str return } - list, err := fs.ReadDir(abspath) + list, err := godoc.FS.ReadDir(abspath) if err != nil { - serveError(w, r, relpath, err) + godoc.ServeError(w, r, relpath, err) return } - servePage(w, Page{ + godoc.ServePage(w, godoc.Page{ Title: "Directory " + relpath, Tabtitle: relpath, - Body: applyTemplate(dirlistHTML, "dirlistHTML", list), + Body: applyTemplate(godoc.DirlistHTML, "dirlistHTML", list), }) } @@ -786,14 +220,14 @@ func serveFile(w http.ResponseWriter, r *http.Request) { relpath := r.URL.Path // Check to see if we need to redirect or serve another file. - if m := metadataFor(relpath); m != nil { + if m := godoc.MetadataFor(relpath); m != nil { if m.Path != relpath { // Redirect to canonical path. http.Redirect(w, r, m.Path, http.StatusMovedPermanently) return } // Serve from the actual filesystem path. - relpath = m.filePath + relpath = m.FilePath() } abspath := relpath @@ -807,7 +241,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) return } - serveHTMLDoc(w, r, abspath, relpath) + godoc.ServeHTMLDoc(w, r, abspath, relpath) return case ".go": @@ -815,10 +249,10 @@ func serveFile(w http.ResponseWriter, r *http.Request) { return } - dir, err := fs.Lstat(abspath) + dir, err := godoc.FS.Lstat(abspath) if err != nil { log.Print(err) - serveError(w, r, relpath, err) + godoc.ServeError(w, r, relpath, err) return } @@ -826,15 +260,15 @@ func serveFile(w http.ResponseWriter, r *http.Request) { if redirect(w, r) { return } - if index := pathpkg.Join(abspath, "index.html"); util.IsTextFile(fs, index) { - serveHTMLDoc(w, r, index, index) + if index := pathpkg.Join(abspath, "index.html"); util.IsTextFile(godoc.FS, index) { + godoc.ServeHTMLDoc(w, r, index, index) return } serveDirectory(w, r, abspath, relpath) return } - if util.IsTextFile(fs, abspath) { + if util.IsTextFile(godoc.FS, abspath) { if redirectFile(w, r) { return } @@ -842,7 +276,7 @@ func serveFile(w http.ResponseWriter, r *http.Request) { return } - fileServer.ServeHTTP(w, r) + godoc.FileServer.ServeHTTP(w, r) } func serveSearchDesc(w http.ResponseWriter, r *http.Request) { @@ -850,7 +284,7 @@ func serveSearchDesc(w http.ResponseWriter, r *http.Request) { data := map[string]interface{}{ "BaseURL": fmt.Sprintf("http://%s", r.Host), } - if err := searchDescXML.Execute(w, &data); err != nil && err != http.ErrBodyNotAllowed { + if err := godoc.SearchDescXML.Execute(w, &data); err != nil && err != http.ErrBodyNotAllowed { // Only log if there's an error that's not about writing on HEAD requests. // See Issues 5451 and 5454. log.Printf("searchDescXML.Execute: %s", err) @@ -860,48 +294,6 @@ func serveSearchDesc(w http.ResponseWriter, r *http.Request) { // ---------------------------------------------------------------------------- // Packages -// Fake relative package path for built-ins. Documentation for all globals -// (not just exported ones) will be shown for packages in this directory. -const builtinPkgPath = "builtin" - -type PageInfoMode uint - -const ( - noFiltering PageInfoMode = 1 << iota // do not filter exports - allMethods // show all embedded methods - showSource // show source code, do not extract documentation - noHtml // show result in textual form, do not generate HTML - flatDir // show directory in a flat (non-indented) manner -) - -// modeNames defines names for each PageInfoMode flag. -var modeNames = map[string]PageInfoMode{ - "all": noFiltering, - "methods": allMethods, - "src": showSource, - "text": noHtml, - "flat": flatDir, -} - -// getPageInfoMode computes the PageInfoMode flags by analyzing the request -// URL form value "m". It is value is a comma-separated list of mode names -// as defined by modeNames (e.g.: m=src,text). -func getPageInfoMode(r *http.Request) PageInfoMode { - var mode PageInfoMode - for _, k := range strings.Split(r.FormValue("m"), ",") { - if m, found := modeNames[strings.TrimSpace(k)]; found { - mode |= m - } - } - return adjustPageInfoMode(r, mode) -} - -// Specialized versions of godoc may adjust the PageInfoMode by overriding -// this variable. -var adjustPageInfoMode = func(_ *http.Request, mode PageInfoMode) PageInfoMode { - return mode -} - // remoteSearchURL returns the search URL for a given query as needed by // remoteSearch. If html is set, an html result is requested; otherwise // the result is in textual form. @@ -915,363 +307,35 @@ func remoteSearchURL(query string, html bool) string { return s + url.QueryEscape(query) } -type PageInfo struct { - Dirname string // directory containing the package - Err error // error or nil - - // package info - FSet *token.FileSet // nil if no package documentation - PDoc *doc.Package // nil if no package documentation - Examples []*doc.Example // nil if no example code - Notes map[string][]*doc.Note // nil if no package Notes - PAst *ast.File // nil if no AST with package exports - IsMain bool // true for package main - - // directory info - Dirs *DirList // nil if no directory information - DirTime time.Time // directory time stamp - DirFlat bool // if set, show directory in a flat (non-indented) manner -} - -func (info *PageInfo) IsEmpty() bool { - return info.Err != nil || info.PAst == nil && info.PDoc == nil && info.Dirs == nil -} - -type docServer struct { - pattern string // url pattern; e.g. "/pkg/" - fsRoot string // file system root to which the pattern is mapped -} - -// fsReadDir implements ReadDir for the go/build package. -func fsReadDir(dir string) ([]os.FileInfo, error) { - return fs.ReadDir(filepath.ToSlash(dir)) -} - -// fsOpenFile implements OpenFile for the go/build package. -func fsOpenFile(name string) (r io.ReadCloser, err error) { - data, err := ReadFile(fs, filepath.ToSlash(name)) - if err != nil { - return nil, err - } - return ioutil.NopCloser(bytes.NewReader(data)), nil -} - -// packageExports is a local implementation of ast.PackageExports -// which correctly updates each package file's comment list. -// (The ast.PackageExports signature is frozen, hence the local -// implementation). -// -func packageExports(fset *token.FileSet, pkg *ast.Package) { - for _, src := range pkg.Files { - cmap := ast.NewCommentMap(fset, src, src.Comments) - ast.FileExports(src) - src.Comments = cmap.Filter(src).Comments() - } -} - -// addNames adds the names declared by decl to the names set. -// Method names are added in the form ReceiverTypeName_Method. -func addNames(names map[string]bool, decl ast.Decl) { - switch d := decl.(type) { - case *ast.FuncDecl: - name := d.Name.Name - if d.Recv != nil { - var typeName string - switch r := d.Recv.List[0].Type.(type) { - case *ast.StarExpr: - typeName = r.X.(*ast.Ident).Name - case *ast.Ident: - typeName = r.Name - } - name = typeName + "_" + name - } - names[name] = true - case *ast.GenDecl: - for _, spec := range d.Specs { - switch s := spec.(type) { - case *ast.TypeSpec: - names[s.Name.Name] = true - case *ast.ValueSpec: - for _, id := range s.Names { - names[id.Name] = true - } - } - } - } -} - -// globalNames returns a set of the names declared by all package-level -// declarations. Method names are returned in the form Receiver_Method. -func globalNames(pkg *ast.Package) map[string]bool { - names := make(map[string]bool) - for _, file := range pkg.Files { - for _, decl := range file.Decls { - addNames(names, decl) - } - } - return names -} - -// collectExamples collects examples for pkg from testfiles. -func collectExamples(pkg *ast.Package, testfiles map[string]*ast.File) []*doc.Example { - var files []*ast.File - for _, f := range testfiles { - files = append(files, f) - } - - var examples []*doc.Example - globals := globalNames(pkg) - for _, e := range doc.Examples(files...) { - name := stripExampleSuffix(e.Name) - if name == "" || globals[name] { - examples = append(examples, e) - } else { - log.Printf("skipping example 'Example%s' because '%s' is not a known function or type", e.Name, e.Name) - } - } - - return examples -} - -// poorMansImporter returns a (dummy) package object named -// by the last path component of the provided package path -// (as is the convention for packages). This is sufficient -// to resolve package identifiers without doing an actual -// import. It never returns an error. -// -func poorMansImporter(imports map[string]*ast.Object, path string) (*ast.Object, error) { - pkg := imports[path] - if pkg == nil { - // note that strings.LastIndex returns -1 if there is no "/" - pkg = ast.NewObj(ast.Pkg, path[strings.LastIndex(path, "/")+1:]) - pkg.Data = ast.NewScope(nil) // required by ast.NewPackage for dot-import - imports[path] = pkg - } - return pkg, nil -} - -// getPageInfo returns the PageInfo for a package directory abspath. If the -// parameter genAST is set, an AST containing only the package exports is -// computed (PageInfo.PAst), otherwise package documentation (PageInfo.Doc) -// is extracted from the AST. If there is no corresponding package in the -// directory, PageInfo.PAst and PageInfo.PDoc are nil. If there are no sub- -// directories, PageInfo.Dirs is nil. If an error occurred, PageInfo.Err is -// set to the respective error but the error is not logged. -// -func (h *docServer) getPageInfo(abspath, relpath string, mode PageInfoMode) *PageInfo { - info := &PageInfo{Dirname: abspath} - - // Restrict to the package files that would be used when building - // the package on this system. This makes sure that if there are - // separate implementations for, say, Windows vs Unix, we don't - // jumble them all together. - // Note: Uses current binary's GOOS/GOARCH. - // To use different pair, such as if we allowed the user to choose, - // set ctxt.GOOS and ctxt.GOARCH before calling ctxt.ImportDir. - ctxt := build.Default - ctxt.IsAbsPath = pathpkg.IsAbs - ctxt.ReadDir = fsReadDir - ctxt.OpenFile = fsOpenFile - pkginfo, err := ctxt.ImportDir(abspath, 0) - // continue if there are no Go source files; we still want the directory info - if _, nogo := err.(*build.NoGoError); err != nil && !nogo { - info.Err = err - return info - } - - // collect package files - pkgname := pkginfo.Name - pkgfiles := append(pkginfo.GoFiles, pkginfo.CgoFiles...) - if len(pkgfiles) == 0 { - // Commands written in C have no .go files in the build. - // Instead, documentation may be found in an ignored file. - // The file may be ignored via an explicit +build ignore - // constraint (recommended), or by defining the package - // documentation (historic). - pkgname = "main" // assume package main since pkginfo.Name == "" - pkgfiles = pkginfo.IgnoredGoFiles - } - - // get package information, if any - if len(pkgfiles) > 0 { - // build package AST - fset := token.NewFileSet() - files, err := parseFiles(fset, abspath, pkgfiles) - if err != nil { - info.Err = err - return info - } - - // ignore any errors - they are due to unresolved identifiers - pkg, _ := ast.NewPackage(fset, files, poorMansImporter, nil) - - // extract package documentation - info.FSet = fset - if mode&showSource == 0 { - // show extracted documentation - var m doc.Mode - if mode&noFiltering != 0 { - m = doc.AllDecls - } - if mode&allMethods != 0 { - m |= doc.AllMethods - } - info.PDoc = doc.New(pkg, pathpkg.Clean(relpath), m) // no trailing '/' in importpath - - // collect examples - testfiles := append(pkginfo.TestGoFiles, pkginfo.XTestGoFiles...) - files, err = parseFiles(fset, abspath, testfiles) - if err != nil { - log.Println("parsing examples:", err) - } - info.Examples = collectExamples(pkg, files) - - // collect any notes that we want to show - if info.PDoc.Notes != nil { - // could regexp.Compile only once per godoc, but probably not worth it - if rx, err := regexp.Compile(*notes); err == nil { - for m, n := range info.PDoc.Notes { - if rx.MatchString(m) { - if info.Notes == nil { - info.Notes = make(map[string][]*doc.Note) - } - info.Notes[m] = n - } - } - } - } - - } else { - // show source code - // TODO(gri) Consider eliminating export filtering in this mode, - // or perhaps eliminating the mode altogether. - if mode&noFiltering == 0 { - packageExports(fset, pkg) - } - info.PAst = ast.MergePackageFiles(pkg, 0) - } - info.IsMain = pkgname == "main" - } - - // get directory information, if any - var dir *Directory - var timestamp time.Time - if tree, ts := fsTree.Get(); tree != nil && tree.(*Directory) != nil { - // directory tree is present; lookup respective directory - // (may still fail if the file system was updated and the - // new directory tree has not yet been computed) - dir = tree.(*Directory).lookup(abspath) - timestamp = ts - } - if dir == nil { - // no directory tree present (too early after startup or - // command-line mode); compute one level for this page - // note: cannot use path filter here because in general - // it doesn't contain the fsTree path - dir = newDirectory(abspath, 1) - timestamp = time.Now() - } - info.Dirs = dir.listing(true) - info.DirTime = timestamp - info.DirFlat = mode&flatDir != 0 - - return info -} - -func (h *docServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if redirect(w, r) { - return - } - - relpath := pathpkg.Clean(r.URL.Path[len(h.pattern):]) - abspath := pathpkg.Join(h.fsRoot, relpath) - mode := getPageInfoMode(r) - if relpath == builtinPkgPath { - mode = noFiltering - } - info := h.getPageInfo(abspath, relpath, mode) - if info.Err != nil { - log.Print(info.Err) - serveError(w, r, relpath, info.Err) - return - } - - if mode&noHtml != 0 { - serveText(w, applyTemplate(packageText, "packageText", info)) - return - } - - var tabtitle, title, subtitle string - switch { - case info.PAst != nil: - tabtitle = info.PAst.Name.Name - case info.PDoc != nil: - tabtitle = info.PDoc.Name - default: - tabtitle = info.Dirname - title = "Directory " - if *showTimestamps { - subtitle = "Last update: " + info.DirTime.String() - } - } - if title == "" { - if info.IsMain { - // assume that the directory name is the command name - _, tabtitle = pathpkg.Split(relpath) - title = "Command " - } else { - title = "Package " - } - } - title += tabtitle - - // special cases for top-level package/command directories - switch tabtitle { - case "/src/pkg": - tabtitle = "Packages" - case "/src/cmd": - tabtitle = "Commands" - } - - servePage(w, Page{ - Title: title, - Tabtitle: tabtitle, - Subtitle: subtitle, - Body: applyTemplate(packageHTML, "packageHTML", info), - }) -} - // ---------------------------------------------------------------------------- // Search -var searchIndex util.RWValue - type SearchResult struct { Query string Alert string // error or warning message // identifier matches - Pak HitList // packages matching Query - Hit *LookupResult // identifier matches of Query - Alt *AltWords // alternative identifiers to look for + Pak godoc.HitList // packages matching Query + Hit *godoc.LookupResult // identifier matches of Query + Alt *godoc.AltWords // alternative identifiers to look for // textual matches - Found int // number of textual occurrences found - Textual []FileLines // textual matches of Query - Complete bool // true if all textual occurrences of Query are reported + Found int // number of textual occurrences found + Textual []godoc.FileLines // textual matches of Query + Complete bool // true if all textual occurrences of Query are reported } func lookup(query string) (result SearchResult) { result.Query = query - index, timestamp := searchIndex.Get() + index, timestamp := godoc.SearchIndex.Get() if index != nil { - index := index.(*Index) + index := index.(*godoc.Index) // identifier search var err error result.Pak, result.Hit, result.Alt, err = index.Lookup(query) - if err != nil && *maxResults <= 0 { + if err != nil && godoc.MaxResults <= 0 { // ignore the error if full text search is enabled // since the query may be a valid regular expression result.Alert = "Error in query string: " + err.Error() @@ -1279,7 +343,7 @@ func lookup(query string) (result SearchResult) { } // full text search - if *maxResults > 0 && query != "" { + if godoc.MaxResults > 0 && query != "" { rx, err := regexp.Compile(query) if err != nil { result.Alert = "Error in query regular expression: " + err.Error() @@ -1289,8 +353,8 @@ func lookup(query string) (result SearchResult) { // maxResults results and thus the result may be incomplete (to be // precise, we should remove one result from the result set, but // nobody is going to count the results on the result page). - result.Found, result.Textual = index.LookupRegexp(rx, *maxResults+1) - result.Complete = result.Found <= *maxResults + result.Found, result.Textual = index.LookupRegexp(rx, godoc.MaxResults+1) + result.Complete = result.Found <= godoc.MaxResults if !result.Complete { result.Found-- // since we looked for maxResults+1 } @@ -1298,8 +362,8 @@ func lookup(query string) (result SearchResult) { } // is the result accurate? - if *indexEnabled { - if _, ts := fsModified.Get(); timestamp.Before(ts) { + if godoc.IndexEnabled { + if _, ts := godoc.FSModified.Get(); timestamp.Before(ts) { // The index is older than the latest file system change under godoc's observation. result.Alert = "Indexing in progress: result may be inaccurate" } @@ -1314,8 +378,8 @@ func search(w http.ResponseWriter, r *http.Request) { query := strings.TrimSpace(r.FormValue("q")) result := lookup(query) - if getPageInfoMode(r)&noHtml != 0 { - serveText(w, applyTemplate(searchText, "searchText", result)) + if godoc.GetPageInfoMode(r)&godoc.NoHTML != 0 { + godoc.ServeText(w, applyTemplate(godoc.SearchText, "searchText", result)) return } @@ -1326,249 +390,10 @@ func search(w http.ResponseWriter, r *http.Request) { title = fmt.Sprintf(`No results found for query %q`, query) } - servePage(w, Page{ + godoc.ServePage(w, godoc.Page{ Title: title, Tabtitle: query, Query: query, - Body: applyTemplate(searchHTML, "searchHTML", result), + Body: applyTemplate(godoc.SearchHTML, "searchHTML", result), }) } - -// ---------------------------------------------------------------------------- -// Documentation Metadata - -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 -} - -// extractMetadata extracts the Metadata from a byte slice. -// It returns the Metadata value and the remaining data. -// If no metadata is present the original byte slice is returned. -// -func extractMetadata(b []byte) (meta Metadata, tail []byte, err error) { - tail = b - if !bytes.HasPrefix(b, jsonStart) { - return - } - end := bytes.Index(b, jsonEnd) - if end < 0 { - return - } - b = b[len(jsonStart)-1 : end+1] // drop leading