mirror of
https://github.com/golang/go
synced 2024-10-03 16:31:27 -06:00
e97121a187
add comment describing new web server tree. make room for command line interface. use new path package to get rid of doubled slashes. use new Chdir function to avoid goroot + everything. implement new /pkg/ tree instead of using regexps. R=gri DELTA=267 (103 added, 72 deleted, 92 changed) OCL=27150 CL=27367
659 lines
15 KiB
Go
659 lines
15 KiB
Go
// 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, "<a href=\"%s\">%s</a><br />\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, "<!--", template.Substitution {
|
|
"TITLE-->" : 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, "<h2>Directories</h2>");
|
|
for i, entry := range list {
|
|
if entry.IsDirectory() {
|
|
printLink(c, path, entry.Name);
|
|
}
|
|
}
|
|
|
|
fmt.Fprintln(c, "<h2>Go files</h2>");
|
|
for i, entry := range list {
|
|
if isGoFile(&entry) {
|
|
printLink(c, path, entry.Name);
|
|
}
|
|
}
|
|
|
|
fmt.Fprintln(c, "<h2>Other files</h2>");
|
|
for i, entry := range list {
|
|
if !entry.IsDirectory() && !isGoFile(&entry) {
|
|
fmt.Fprintf(c, "%s<br />\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, "<h1>Parse errors in %s</h1>\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, "<pre>");
|
|
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, "<b><font color=red>%s >>></font></b>", 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, "</pre>");
|
|
});
|
|
}
|
|
|
|
|
|
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, "<pre>");
|
|
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, "</pre>");
|
|
});
|
|
}
|
|
|
|
|
|
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, "<a href=\"%s\">%s</a> <font color=grey>(%s)</font><br />\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");
|
|
}
|