// Copyright 2009 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. // godoc: Go Documentation Server // Web server tree: // // http://godoc/ main landing page (TODO) // http://godoc/doc/ serve from $GOROOT/doc - spec, mem, tutorial, etc. (TODO) // http://godoc/src/ serve files from $GOROOT/src; .go gets pretty-printed // http://godoc/cmd/ serve documentation about commands (TODO) // http://godoc/pkg/ serve documentation about packages // (idea is if you say import "compress/zlib", you go to // http://godoc/pkg/compress/zlib) // // Command-line interface: // // godoc packagepath [name ...] // // godoc compress/zlib // - prints doc for package proto // godoc compress/zlib Cipher NewCMAC // - prints doc for Cipher and NewCMAC in package crypto/block package main import ( "ast"; "bufio"; "flag"; "fmt"; "http"; "io"; "log"; "net"; "os"; "parser"; pathutil "path"; "sort"; "tabwriter"; "template"; "time"; "token"; "vector"; "astprinter"; "docprinter"; ) // TODO // - uniform use of path, filename, dirname, pakname, etc. // - fix weirdness with double-/'s in paths // - split http service into its own source file const usageString = "usage: godoc package [name ...]\n" " godoc -http=:6060\n" const ( docPrefix = "/doc/"; filePrefix = "/file/"; ) var ( goroot string; verbose = flag.Bool("v", false, "verbose mode"); // server control httpaddr = flag.String("http", "", "HTTP service address (e.g., ':6060')"); // layout control tabwidth = flag.Int("tabwidth", 4, "tab width"); usetabs = flag.Bool("tabs", false, "align with tabs instead of spaces"); ) func init() { var err *os.Error; goroot, err = os.Getenv("GOROOT"); if err != nil { goroot = "/home/r/go-build/go"; } flag.StringVar(&goroot, "goroot", goroot, "Go root directory"); } // ---------------------------------------------------------------------------- // Support func hasPrefix(s, prefix string) bool { return len(prefix) <= len(s) && s[0 : len(prefix)] == prefix; } func hasSuffix(s, suffix string) bool { pos := len(s) - len(suffix); return pos >= 0 && s[pos : len(s)] == suffix; } func isGoFile(dir *os.Dir) bool { return dir.IsRegular() && hasSuffix(dir.Name, ".go"); } func isHTMLFile(dir *os.Dir) bool { return dir.IsRegular() && hasSuffix(dir.Name, ".html"); } func isDir(name string) bool { d, err := os.Stat(name); return err == nil && d.IsDirectory(); } func isFile(name string) bool { d, err := os.Stat(name); return err == nil && d.IsRegular(); } func printLink(c *http.Conn, dir, name string) { fmt.Fprintf(c, "%s
\n", pathutil.Clean(filePrefix + dir + "/" + name), name); } func makeTabwriter(writer io.Write) *tabwriter.Writer { padchar := byte(' '); if *usetabs { padchar = '\t'; } return tabwriter.NewWriter(writer, *tabwidth, 1, padchar, tabwriter.FilterHTML); } // ---------------------------------------------------------------------------- // Parsing type parseError struct { pos token.Position; msg string; } type errorList []parseError func (list errorList) Len() int { return len(list); } func (list errorList) Less(i, j int) bool { return list[i].pos.Offset < list[j].pos.Offset; } func (list errorList) Swap(i, j int) { list[i], list[j] = list[j], list[i]; } type errorHandler struct { lastLine int; errors *vector.Vector; } func (h *errorHandler) Error(pos token.Position, msg string) { // only collect errors that are on a new line // in the hope to avoid most follow-up errors if pos.Line != h.lastLine { h.lastLine = pos.Line; if h.errors == nil { // lazy initialize - most of the time there are no errors h.errors = vector.New(0); } h.errors.Push(parseError{pos, msg}); } } // Parses a file (path) and returns the corresponding AST and // a sorted list (by file position) of errors, if any. // func parse(path string, mode uint) (*ast.Program, errorList) { src, err := os.Open(path, os.O_RDONLY, 0); defer src.Close(); if err != nil { log.Stdoutf("open %s: %v", path, err); var noPos token.Position; return nil, errorList{parseError{noPos, err.String()}}; } var handler errorHandler; prog, ok := parser.Parse(src, &handler, mode); if !ok { // convert error list and sort it errors := make(errorList, handler.errors.Len()); for i := 0; i < handler.errors.Len(); i++ { errors[i] = handler.errors.At(i).(parseError); } sort.Sort(errors); return nil, errors; } return prog, nil; } // ---------------------------------------------------------------------------- // Templates // html template // TODO initialize only if needed (i.e. if run as a server) var godoc_html = template.NewTemplateOrDie("godoc.html"); func servePage(c *http.Conn, title string, contents func()) { c.SetHeader("content-type", "text/html; charset=utf-8"); // TODO handle Apply errors godoc_html.Apply(c, "" : func() { fmt.Fprint(c, title); }, "HEADER-->" : func() { fmt.Fprint(c, title); }, "TIMESTAMP-->" : func() { fmt.Fprint(c, time.UTC().String()); }, "CONTENTS-->" : contents }); } func serveError(c *http.Conn, err, arg string) { servePage(c, "Error", func () { fmt.Fprintf(c, "%v (%s)\n", err, arg); }); } // ---------------------------------------------------------------------------- // Directories type dirArray []os.Dir func (p dirArray) Len() int { return len(p); } func (p dirArray) Less(i, j int) bool { return p[i].Name < p[j].Name; } func (p dirArray) Swap(i, j int) { p[i], p[j] = p[j], p[i]; } func serveDir(c *http.Conn, dirname string) { fd, err1 := os.Open(dirname, os.O_RDONLY, 0); if err1 != nil { c.WriteHeader(http.StatusNotFound); fmt.Fprintf(c, "Error: %v (%s)\n", err1, dirname); return; } list, err2 := fd.Readdir(-1); if err2 != nil { c.WriteHeader(http.StatusNotFound); fmt.Fprintf(c, "Error: %v (%s)\n", err2, dirname); return; } sort.Sort(dirArray(list)); path := dirname + "/"; // Print contents in 3 sections: directories, go files, everything else servePage(c, dirname + " - Contents", func () { fmt.Fprintln(c, "

Directories

"); for i, entry := range list { if entry.IsDirectory() { printLink(c, path, entry.Name); } } fmt.Fprintln(c, "

Go files

"); for i, entry := range list { if isGoFile(&entry) { printLink(c, path, entry.Name); } } fmt.Fprintln(c, "

Other files

"); for i, entry := range list { if !entry.IsDirectory() && !isGoFile(&entry) { fmt.Fprintf(c, "%s
\n", entry.Name); } } }); } // ---------------------------------------------------------------------------- // Files func serveParseErrors(c *http.Conn, filename string, errors errorList) { // open file path := filename; fd, err1 := os.Open(path, os.O_RDONLY, 0); defer fd.Close(); if err1 != nil { serveError(c, err1.String(), path); return; } // read source var buf io.ByteBuffer; n, err2 := io.Copy(fd, &buf); if err2 != nil { serveError(c, err2.String(), path); return; } src := buf.Data(); // TODO handle Apply errors servePage(c, filename, func () { // section title fmt.Fprintf(c, "

Parse errors in %s

\n", filename); // handle read errors if err1 != nil || err2 != nil { fmt.Fprintf(c, "could not read file %s\n", filename); return; } // write source with error messages interspersed fmt.Fprintln(c, "
");
		offs := 0;
		for i, e := range errors {
			if 0 <= e.pos.Offset && e.pos.Offset <= len(src) {
				// TODO handle Write errors
				c.Write(src[offs : e.pos.Offset]);
				// TODO this should be done using a .css file
				fmt.Fprintf(c, "%s >>>", e.msg);
				offs = e.pos.Offset;
			} else {
				log.Stdoutf("error position %d out of bounds (len = %d)", e.pos.Offset, len(src));
			}
		}
		// TODO handle Write errors
		c.Write(src[offs : len(src)]);
		fmt.Fprintln(c, "
"); }); } func serveGoSource(c *http.Conn, dirname string, filename string) { path := dirname + "/" + filename; prog, errors := parse(path, parser.ParseComments); if len(errors) > 0 { serveParseErrors(c, filename, errors); return; } servePage(c, path + " - Go source", func () { fmt.Fprintln(c, "
");
		var p astPrinter.Printer;
		writer := makeTabwriter(c);  // for nicely formatted output
		p.Init(writer, nil, nil, true);
		p.DoProgram(prog);
		writer.Flush();  // ignore errors
		fmt.Fprintln(c, "
"); }); } func serveHTMLFile(c *http.Conn, filename string) { src, err1 := os.Open(filename, os.O_RDONLY, 0); defer src.Close(); if err1 != nil { serveError(c, err1.String(), filename); return } if written, err2 := io.Copy(src, c); err2 != nil { serveError(c, err2.String(), filename); return } } func serveFile(c *http.Conn, path string) { dir, err := os.Stat(path); if err != nil { serveError(c, err.String(), path); return; } switch { case dir.IsDirectory(): serveDir(c, path); case isGoFile(dir): serveGoSource(c, ".", path); case isHTMLFile(dir): serveHTMLFile(c, path); default: serveError(c, "Not a directory or .go file", path); } } // ---------------------------------------------------------------------------- // Packages type pakDesc struct { dirname string; // relative to goroot pakname string; // relative to directory filenames map[string] bool; // set of file (names) belonging to this package } type pakArray []*pakDesc func (p pakArray) Len() int { return len(p); } func (p pakArray) Less(i, j int) bool { return p[i].pakname < p[j].pakname; } func (p pakArray) Swap(i, j int) { p[i], p[j] = p[j], p[i]; } func addFile(pmap map[string]*pakDesc, dirname string, filename string) { if hasSuffix(filename, "_test.go") { // ignore package tests return; } // determine package name path := dirname + "/" + filename; prog, errors := parse(path, parser.PackageClauseOnly); if prog == nil { return; } if prog.Name.Value == "main" { // ignore main packages for now return; } pakname := pathutil.Clean(dirname + "/" + prog.Name.Value); // find package descriptor pakdesc, found := pmap[pakname]; if !found { // add a new descriptor pakdesc = &pakDesc{dirname, prog.Name.Value, make(map[string]bool)}; pmap[pakname] = pakdesc; } //fmt.Printf("pak = %s, file = %s\n", pakname, filename); // add file to package desc if tmp, found := pakdesc.filenames[filename]; found { panic("internal error: same file added more then once: " + filename); } pakdesc.filenames[filename] = true; } func addDirectory(pmap map[string]*pakDesc, dirname string) { path := dirname; fd, err1 := os.Open(path, os.O_RDONLY, 0); if err1 != nil { log.Stdoutf("open %s: %v", path, err1); return; } list, err2 := fd.Readdir(-1); if err2 != nil { log.Stdoutf("readdir %s: %v", path, err2); return; } for i, entry := range list { switch { case isGoFile(&entry): //fmt.Printf("found %s/%s\n", dirname, entry.Name); addFile(pmap, dirname, entry.Name); } } } func mapValues(pmap map[string]*pakDesc) pakArray { // build sorted package list plist := make(pakArray, len(pmap)); i := 0; for tmp, pakdesc := range pmap { plist[i] = pakdesc; i++; } sort.Sort(plist); return plist; } func servePackage(c *http.Conn, p *pakDesc) { // make a filename list filenames := make([]string, len(p.filenames)); i := 0; for filename, tmp := range p.filenames { filenames[i] = filename; i++; } // compute documentation var doc docPrinter.PackageDoc; for i, filename := range filenames { path := p.dirname + "/" + filename; prog, errors := parse(path, parser.ParseComments); if len(errors) > 0 { serveParseErrors(c, filename, errors); return; } if i == 0 { // first package - initialize docPrinter doc.Init(prog.Name.Value); } doc.AddProgram(prog); } servePage(c, doc.PackageName() + " - Go package documentation", func () { writer := makeTabwriter(c); // for nicely formatted output doc.Print(writer); writer.Flush(); // ignore errors }); } func servePackageList(c *http.Conn, list pakArray) { servePage(c, "Packages", func () { for i := 0; i < len(list); i++ { p := list[i]; link := pathutil.Clean(p.dirname + "/" + p.pakname); fmt.Fprintf(c, "%s (%s)
\n", p.pakname, p.pakname, link); } }); // TODO: show subdirectories } // Return package or packages named by name. // Name is either an import string or a directory, // like you'd see in $GOROOT/pkg/ once the 6g // tools can handle a hierarchy there. // // Examples: // "math" - single package made up of directory // "container" - directory listing // "container/vector" - single package in container directory func findPackages(name string) (*pakDesc, pakArray) { // Build list of packages. // If the path names a directory, scan that directory // for a package with the name matching the directory name. // Otherwise assume it is a package name inside // a directory, so scan the parent. pmap := make(map[string]*pakDesc); dir := pathutil.Clean("src/lib/" + name); if isDir(dir) { parent, pak := pathutil.Split(dir); addDirectory(pmap, dir); paks := mapValues(pmap); if len(paks) == 1 { p := paks[0]; if p.dirname == dir && p.pakname == pak { return p, nil; } } return nil, paks; } // Otherwise, have parentdir/pak. Look for package pak in dir. parentdir, pak := pathutil.Split(dir); addDirectory(pmap, parentdir); if p, ok := pmap[dir]; ok { return p, nil; } return nil, nil; } func servePkg(c *http.Conn, path string) { pak, paks := findPackages(path); // TODO: canonicalize path and redirect if needed. switch { case pak != nil: servePackage(c, pak); case len(paks) > 0: servePackageList(c, paks); default: serveError(c, "No packages found", path); } } // ---------------------------------------------------------------------------- // Server func makeFixedFileServer(filename string) (func(c *http.Conn, path string)) { return func(c *http.Conn, path string) { serveFile(c, filename); }; } func installHandler(prefix string, handler func(c *http.Conn, path string)) { // create a handler customized with prefix f := func(c *http.Conn, req *http.Request) { path := req.Url.Path; if *verbose { log.Stdoutf("%s\t%s", req.Host, path); } handler(c, path[len(prefix) : len(path)]); }; // install the customized handler http.Handle(prefix, http.HandlerFunc(f)); } func usage() { fmt.Fprintf(os.Stderr, usageString); sys.Exit(1); } func main() { flag.Parse(); // Check usage first; get usage message out early. switch { case *httpaddr != "": if flag.NArg() != 0 { usage(); } default: if flag.NArg() == 0 { usage(); } } if err := os.Chdir(goroot); err != nil { log.Exitf("chdir %s: %v", goroot, err); } if *httpaddr != "" { if *verbose { log.Stdoutf("Go Documentation Server\n"); log.Stdoutf("address = %s\n", *httpaddr); log.Stdoutf("goroot = %s\n", goroot); } installHandler("/mem", makeFixedFileServer("doc/go_mem.html")); installHandler("/spec", makeFixedFileServer("doc/go_spec.html")); installHandler("/pkg/", servePkg); installHandler(filePrefix, serveFile); if err := http.ListenAndServe(*httpaddr, nil); err != nil { log.Exitf("ListenAndServe %s: %v", *httpaddr, err) } return; } log.Exitf("godoc command-line not implemented"); }