1
0
mirror of https://github.com/golang/go synced 2024-11-25 07:27:57 -07:00
go/usr/gri/pretty/godoc.go
Russ Cox 1605176e25 godoc: use data-driven templates for html, text generation
R=gri
DELTA=1341  (668 added, 282 deleted, 391 changed)
OCL=27485
CL=27526
2009-04-15 18:53:43 -07:00

779 lines
18 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
// http://godoc/doc/ serve from $GOROOT/doc - spec, mem, tutorial, etc.
// 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";
"comment";
"docprinter"; // TODO: "doc"
)
// TODO
// - uniform use of path, filename, dirname, pakname, etc.
// - fix weirdness with double-/'s in paths
// - split http service into its own source file
// TODO: tell flag package about usage string
const usageString =
"usage: godoc package [name ...]\n"
" godoc -http=:6060\n"
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");
html = flag.Bool("html", false, "print HTML in command-line mode");
pkgroot = flag.String("pkgroot", "src/lib", "root package source directory (if unrooted, relative to goroot)");
)
const (
Pkg = "/pkg/" // name for auto-generated package documentation tree
)
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 isDir(name string) bool {
d, err := os.Stat(name);
return err == nil && d.IsDirectory();
}
func makeTabwriter(writer io.Write) *tabwriter.Writer {
padchar := byte(' ');
if *usetabs {
padchar = '\t';
}
return tabwriter.NewWriter(writer, *tabwidth, 1, padchar, tabwriter.FilterHTML);
}
// TODO(rsc): this belongs in a library somewhere, maybe os
func ReadFile(name string) ([]byte, *os.Error) {
f, err := os.Open(name, os.O_RDONLY, 0);
if err != nil {
return nil, err;
}
defer f.Close();
var b io.ByteBuffer;
if n, err := io.Copy(f, &b); err != nil {
return nil, err;
}
return b.Data(), nil;
}
// ----------------------------------------------------------------------------
// Parsing
type rawError struct {
pos token.Position;
msg string;
}
type rawErrorVector struct {
vector.Vector;
}
func (v *rawErrorVector) At(i int) rawError { return v.Vector.At(i).(rawError) }
func (v *rawErrorVector) Less(i, j int) bool { return v.At(i).pos.Offset < v.At(j).pos.Offset; }
func (v *rawErrorVector) Error(pos token.Position, msg string) {
// only collect errors that are on a new line
// in the hope to avoid most follow-up errors
lastLine := 0;
if n := v.Len(); n > 0 {
lastLine = v.At(n - 1).pos.Line;
}
if lastLine != pos.Line {
v.Push(rawError{pos, msg});
}
}
// A single error in the parsed file.
type parseError struct {
src []byte; // source before error
line int; // line number of error
msg string; // error message
}
// All the errors in the parsed file, plus surrounding source code.
// Each error has a slice giving the source text preceding it
// (starting where the last error occurred). The final element in list[]
// has msg = "", to give the remainder of the source code.
// This data structure is handed to the templates parseerror.txt and parseerror.html.
type parseErrors struct {
filename string; // path to file
list []parseError; // the errors
src []byte; // the file's entire source code
}
// Parses a file (path) and returns the corresponding AST and
// a sorted list (by file position) of errors, if any.
//
func parse(filename string, mode uint) (*ast.Program, *parseErrors) {
src, err := ReadFile(filename);
if err != nil {
log.Stderrf("ReadFile %s: %v", filename, err);
errs := []parseError{parseError{nil, 0, err.String()}};
return nil, &parseErrors{filename, errs, nil};
}
var raw rawErrorVector;
prog, ok := parser.Parse(src, &raw, mode);
if !ok {
// sort and convert error list
sort.Sort(&raw);
errs := make([]parseError, raw.Len() + 1); // +1 for final fragment of source
offs := 0;
for i := 0; i < raw.Len(); i++ {
r := raw.At(i);
// Should always be true, but check for robustness.
if 0 <= r.pos.Offset && r.pos.Offset <= len(src) {
errs[i].src = src[offs : r.pos.Offset];
offs = r.pos.Offset;
}
errs[i].line = r.pos.Line;
errs[i].msg = r.msg;
}
errs[raw.Len()].src = src[offs : len(src)];
return nil, &parseErrors{filename, errs, src};
}
return prog, nil;
}
// ----------------------------------------------------------------------------
// Templates
// Return text for decl.
func DeclText(d ast.Decl) []byte {
var b io.ByteBuffer;
var p astPrinter.Printer;
p.Init(&b, nil, nil, false);
d.Visit(&p);
return b.Data();
}
// Return text for expr.
func ExprText(d ast.Expr) []byte {
var b io.ByteBuffer;
var p astPrinter.Printer;
p.Init(&b, nil, nil, false);
d.Visit(&p);
return b.Data();
}
// Convert x, whatever it is, to text form.
func toText(x interface{}) []byte {
type String interface { String() string }
switch v := x.(type) {
case []byte:
return v;
case string:
return io.StringBytes(v);
case String:
return io.StringBytes(v.String());
case ast.Decl:
return DeclText(v);
case ast.Expr:
return ExprText(v);
}
var b io.ByteBuffer;
fmt.Fprint(&b, x);
return b.Data();
}
// Template formatter for "html" format.
func htmlFmt(w io.Write, x interface{}, format string) {
// Can do better than text in some cases.
switch v := x.(type) {
case ast.Decl:
var p astPrinter.Printer;
tw := makeTabwriter(w);
p.Init(tw, nil, nil, true);
v.Visit(&p);
tw.Flush();
case ast.Expr:
var p astPrinter.Printer;
tw := makeTabwriter(w);
p.Init(tw, nil, nil, true);
v.Visit(&p);
tw.Flush();
default:
template.HtmlEscape(w, toText(x));
}
}
// Template formatter for "html-comment" format.
func htmlCommentFmt(w io.Write, x interface{}, format string) {
comment.ToHtml(w, toText(x));
}
// Template formatter for "" (default) format.
func textFmt(w io.Write, x interface{}, format string) {
w.Write(toText(x));
}
// Template formatter for "dir/" format.
// Writes out "/" if the os.Dir argument is a directory.
var slash = io.StringBytes("/");
func dirSlashFmt(w io.Write, x interface{}, format string) {
d := x.(os.Dir); // TODO(rsc): want *os.Dir
if d.IsDirectory() {
w.Write(slash);
}
}
var fmap = template.FormatterMap{
"": textFmt,
"html": htmlFmt,
"html-comment": htmlCommentFmt,
"dir/": dirSlashFmt,
}
// TODO: const templateDir = "lib/godoc"
const templateDir = "usr/gri/pretty"
func ReadTemplate(name string) *template.Template {
data, err := ReadFile(templateDir + "/" + name);
if err != nil {
log.Exitf("ReadFile %s: %v", name, err);
}
t, err1, line := template.Parse(string(data), fmap);
if err1 != nil {
log.Exitf("%s:%d: %v", name, line, err);
}
return t;
}
var godocHtml *template.Template
var packageHtml *template.Template
var packageText *template.Template
var packagelistHtml *template.Template;
var packagelistText *template.Template;
var parseerrorHtml *template.Template;
var parseerrorText *template.Template;
func ReadTemplates() {
// have to delay until after flags processing,
// so that main has chdir'ed to goroot.
godocHtml = ReadTemplate("godoc.html");
packageHtml = ReadTemplate("package.html");
packageText = ReadTemplate("package.txt");
packagelistHtml = ReadTemplate("packagelist.html");
packagelistText = ReadTemplate("packagelist.txt");
parseerrorHtml = ReadTemplate("parseerror.html");
parseerrorText = ReadTemplate("parseerror.txt");
}
// ----------------------------------------------------------------------------
// Generic HTML wrapper
func servePage(c *http.Conn, title, content interface{}) {
type Data struct {
title interface{};
header interface{};
timestamp string;
content interface{};
}
var d Data;
d.title = title;
d.header = title;
d.timestamp = time.UTC().String();
d.content = content;
godocHtml.Execute(&d, c);
}
func serveText(c *http.Conn, text []byte) {
c.SetHeader("content-type", "text/plain; charset=utf-8");
c.Write(text);
}
func serveError(c *http.Conn, err, arg string) {
servePage(c, "Error", fmt.Sprintf("%v (%s)\n", err, arg));
}
// ----------------------------------------------------------------------------
// Files
func serveParseErrors(c *http.Conn, errors *parseErrors) {
// format errors
var b io.ByteBuffer;
parseerrorHtml.Execute(errors, &b);
servePage(c, errors.filename + " - Parse Errors", b.Data());
}
func serveGoSource(c *http.Conn, name string) {
prog, errors := parse(name, parser.ParseComments);
if errors != nil {
serveParseErrors(c, errors);
return;
}
var b io.ByteBuffer;
fmt.Fprintln(&b, "<pre>");
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, "</pre>");
servePage(c, name + " - Go source", b.Data());
}
var fileServer = http.FileServer(".", "");
func serveFile(c *http.Conn, req *http.Request) {
// pick off special cases and hand the rest to the standard file server
switch {
case req.Url.Path == "/":
// serve landing page.
// TODO: hide page from ordinary file serving.
// writing doc/index.html will take care of that.
http.ServeFile(c, req, "doc/root.html");
case req.Url.Path == "/doc/root.html":
// hide landing page from its real name
http.NotFound(c, req);
case pathutil.Ext(req.Url.Path) == ".go":
serveGoSource(c, req.Url.Path[1:len(req.Url.Path)]);
default:
fileServer.ServeHTTP(c, req);
}
}
// ----------------------------------------------------------------------------
// Packages
type pakDesc struct {
dirname string; // relative to goroot
pakname string; // relative to directory
importpath string; // import "___"
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, filename, importprefix 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;
}
var importpath string;
dir, name := pathutil.Split(importprefix);
if name == prog.Name.Value { // package math in directory "math"
importpath = importprefix;
} else {
importpath = pathutil.Clean(importprefix + "/" + prog.Name.Value);
}
// find package descriptor
pakdesc, found := pmap[importpath];
if !found {
// add a new descriptor
pakdesc = &pakDesc{dirname, prog.Name.Value, importpath, make(map[string]bool)};
pmap[importpath] = 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, importprefix string, subdirs *[]os.Dir) {
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;
}
nsub := 0;
for i, entry := range list {
switch {
case isGoFile(&entry):
addFile(pmap, dirname, entry.Name, importprefix);
case entry.IsDirectory():
nsub++;
}
}
if subdirs != nil && nsub > 0 {
*subdirs = make([]os.Dir, nsub);
nsub = 0;
for i, entry := range list {
if entry.IsDirectory() {
subdirs[nsub] = entry;
nsub++;
}
}
}
}
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 (p *pakDesc) Doc() (*doc.PackageDoc, *parseErrors) {
// compute documentation
var r doc.DocReader;
i := 0;
for filename := range p.filenames {
path := p.dirname + "/" + filename;
prog, err := parse(path, parser.ParseComments);
if err != nil {
return nil, err;
}
if i == 0 {
// first file - initialize doc
r.Init(prog.Name.Value, p.importpath);
}
i++;
r.AddProgram(prog);
}
return r.Doc(), nil;
}
func servePackage(c *http.Conn, p *pakDesc) {
doc, errors := p.Doc();
if errors != nil {
serveParseErrors(c, errors);
return;
}
var b io.ByteBuffer;
if false { // TODO req.Params["format"] == "text"
err := packageText.Execute(doc, &b);
if err != nil {
log.Stderrf("packageText.Execute: %s", err);
}
serveText(c, b.Data());
return;
}
err := packageHtml.Execute(doc, &b);
if err != nil {
log.Stderrf("packageHtml.Execute: %s", err);
}
servePage(c, doc.ImportPath + " - Go package documentation", b.Data());
}
type pakInfo struct {
Path string;
Package *pakDesc;
Packages pakArray;
Subdirs []os.Dir; // TODO(rsc): []*os.Dir
}
func servePackageList(c *http.Conn, info *pakInfo) {
var b io.ByteBuffer;
err := packagelistHtml.Execute(info, &b);
if err != nil {
log.Stderrf("packagelistHtml.Execute: %s", err);
}
servePage(c, info.Path + " - Go packages", b.Data());
}
// 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) *pakInfo {
info := new(pakInfo);
// 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);
cname := pathutil.Clean(name);
if cname == "" {
cname = "."
}
dir := pathutil.Join(*pkgroot, cname);
url := pathutil.Join(Pkg, cname);
if isDir(dir) {
parent, pak := pathutil.Split(dir);
addDirectory(pmap, dir, cname, &info.Subdirs);
paks := mapValues(pmap);
if len(paks) == 1 {
p := paks[0];
if p.dirname == dir && p.pakname == pak {
info.Package = p;
info.Path = cname;
return info;
}
}
info.Packages = paks;
if cname == "." {
info.Path = "";
} else {
info.Path = cname + "/";
}
return info;
}
// Otherwise, have parentdir/pak. Look for package pak in dir.
parentdir, pak := pathutil.Split(dir);
parentname, nam := pathutil.Split(cname);
if parentname == "" {
parentname = "."
}
addDirectory(pmap, parentdir, parentname, nil);
if p, ok := pmap[cname]; ok {
info.Package = p;
info.Path = cname;
return info;
}
info.Path = name; // original, uncleaned name
return info;
}
func servePkg(c *http.Conn, r *http.Request) {
path := r.Url.Path;
path = path[len(Pkg) : len(path)];
info := findPackages(path);
if r.Url.Path != Pkg + info.Path {
http.Redirect(c, info.Path);
return;
}
if info.Package != nil {
servePackage(c, info.Package);
} else {
servePackageList(c, info);
}
}
// ----------------------------------------------------------------------------
// Server
func LoggingHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(c *http.Conn, req *http.Request) {
log.Stderrf("%s\t%s", req.Host, req.Url.Path);
h.ServeHTTP(c, req);
})
}
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);
}
ReadTemplates();
if *httpaddr != "" {
var handler http.Handler = http.DefaultServeMux;
if *verbose {
log.Stderrf("Go Documentation Server\n");
log.Stderrf("address = %s\n", *httpaddr);
log.Stderrf("goroot = %s\n", goroot);
handler = LoggingHandler(handler);
}
http.Handle(Pkg, http.HandlerFunc(servePkg));
http.Handle("/", http.HandlerFunc(serveFile));
if err := http.ListenAndServe(*httpaddr, handler); err != nil {
log.Exitf("ListenAndServe %s: %v", *httpaddr, err)
}
return;
}
if *html {
packageText = packageHtml;
packagelistText = packagelistHtml;
parseerrorText = parseerrorHtml;
}
info := findPackages(flag.Arg(0));
if info.Package == nil {
err := packagelistText.Execute(info, os.Stderr);
if err != nil {
log.Stderrf("packagelistText.Execute: %s", err);
}
sys.Exit(1);
}
doc, errors := info.Package.Doc();
if errors != nil {
err := parseerrorText.Execute(errors, os.Stderr);
if err != nil {
log.Stderrf("parseerrorText.Execute: %s", err);
}
sys.Exit(1);
}
if flag.NArg() > 1 {
args := flag.Args();
doc.Filter(args[1:len(args)]);
}
packageText.Execute(doc, os.Stdout);
}