1
0
mirror of https://github.com/golang/go synced 2024-10-03 16:31:27 -06:00
go/usr/gri/pretty/godoc.go
Russ Cox e97121a187 working checkpoint.
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
2009-04-13 13:28:53 -07:00

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");
}