// 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";
"once";
"os";
"parser";
pathutil "path";
"sort";
"strings";
"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 isGoFile(dir *os.Dir) bool {
return dir.IsRegular() && strings.HasSuffix(dir.Name, ".go");
}
func isHTMLFile(dir *os.Dir) bool {
return dir.IsRegular() && strings.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 io.Write, 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.Stderrf("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
var godoc_html string
func readTemplate() {
name := "usr/gri/pretty/godoc.html";
f, err := os.Open(name, os.O_RDONLY, 0);
if err != nil {
log.Exitf("open %s: %v", name, err);
}
var b io.ByteBuffer;
if n, err := io.Copy(f, &b); err != nil {
log.Exitf("copy %s: %v", name, err);
}
f.Close();
godoc_html = string(b.Data());
}
func servePage(c *http.Conn, title, content interface{}) {
once.Do(readTemplate);
c.SetHeader("content-type", "text/html; charset=utf-8");
type Data struct {
title string;
header string;
timestamp string;
content string;
}
// TODO(rsc): Once template system can handle []byte,
// remove this conversion.
if x, ok := title.([]byte); ok {
title = string(x);
}
if x, ok := content.([]byte); ok {
content = string(x);
}
var d Data;
d.title = title.(string);
d.header = title.(string);
d.timestamp = time.UTC().String();
d.content = content.(string);
templ, err, line := template.Parse(godoc_html, nil);
if err != nil {
log.Stderrf("template error %s:%d: %s\n", title, line, err);
} else {
templ.Execute(&d, c);
}
}
func serveError(c *http.Conn, err, arg string) {
servePage(c, "Error", fmt.Sprintf("%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
var b io.ByteBuffer;
fmt.Fprintln(&b, "
"); offs := 0; for i, e := range errors { if 0 <= e.pos.Offset && e.pos.Offset <= len(src) { // TODO handle Write errors b.Write(src[offs : e.pos.Offset]); // TODO this should be done using a .css file fmt.Fprintf(&b, "%s >>>", e.msg); offs = e.pos.Offset; } else { log.Stderrf("error position %d out of bounds (len = %d)", e.pos.Offset, len(src)); } } // TODO handle Write errors b.Write(src[offs : len(src)]); fmt.Fprintln(&b, ""); servePage(c, filename, b.Data()); } 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; } var b io.ByteBuffer; fmt.Fprintln(&b, "
"); var p astPrinter.Printer; writer := makeTabwriter(&b); // for nicely formatted output p.Init(writer, nil, nil, true); p.DoProgram(prog); writer.Flush(); // ignore errors fmt.Fprintln(&b, ""); servePage(c, path + " - Go source", b.Data()); } 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 strings.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.Stderrf("open %s: %v", path, err1); return; } list, err2 := fd.Readdir(-1); if err2 != nil { log.Stderrf("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); } var b io.ByteBuffer; writer := makeTabwriter(&b); // for nicely formatted output doc.Print(writer); writer.Flush(); // ignore errors servePage(c, doc.PackageName() + " - Go package documentation", b.Data()); } func servePackageList(c *http.Conn, list pakArray) { var b io.ByteBuffer; for i := 0; i < len(list); i++ { p := list[i]; link := pathutil.Clean(p.dirname + "/" + p.pakname); fmt.Fprintf(&b, "%s (%s)