godoc: use data-driven templates for html, text generation

R=gri
DELTA=1341  (668 added, 282 deleted, 391 changed)
OCL=27485
CL=27526
diff --git a/src/lib/go/ast.go b/src/lib/go/ast.go
index 9ee88c3..beaa743a 100644
--- a/src/lib/go/ast.go
+++ b/src/lib/go/ast.go
@@ -7,7 +7,11 @@
 //
 package ast
 
-import "token"
+import (
+	"token";
+	"unicode";
+	"utf8";
+)
 
 
 // ----------------------------------------------------------------------------
@@ -45,7 +49,7 @@
 	// visitor v invokes the node-specific DoX function of the visitor.
 	//
 	Visit(v ExprVisitor);
-	
+
 	// Pos returns the (beginning) position of the expression.
 	Pos() token.Position;
 }
@@ -57,7 +61,7 @@
 	// visitor v invokes the node-specific DoX function of the visitor.
 	//
 	Visit(v StmtVisitor);
-	
+
 	// Pos returns the (beginning) position of the statement.
 	Pos() token.Position;
 }
@@ -69,7 +73,7 @@
 	// visitor v invokes the node-specific DoX function of the visitor.
 	//
 	Visit(v DeclVisitor);
-	
+
 	// Pos returns the (beginning) position of the declaration.
 	Pos() token.Position;
 }
@@ -419,6 +423,24 @@
 func (x *ChanType) Visit(v ExprVisitor) { v.DoChanType(x); }
 
 
+// IsExported returns whether name is an exported Go symbol
+// (i.e., whether it begins with an uppercase letter).
+func IsExported(name string) bool {
+	ch, len := utf8.DecodeRuneInString(name, 0);
+	return unicode.IsUpper(ch);
+}
+
+// IsExported returns whether name is an exported Go symbol
+// (i.e., whether it begins with an uppercase letter).
+func (name *ast.Ident) IsExported() bool {
+	return IsExported(name.Value);
+}
+
+func (name *ast.Ident) String() string {
+	return name.Value;
+}
+
+
 // ----------------------------------------------------------------------------
 // Statements
 
@@ -688,7 +710,7 @@
 
 // A declaration is represented by one of the following declaration nodes.
 //
-type (	
+type (
 	// A BadDecl node is a placeholder for declarations containing
 	// syntax errors for which no correct declaration nodes can be
 	// created.
diff --git a/src/run.bash b/src/run.bash
index 3053e7d..de2bd7e 100755
--- a/src/run.bash
+++ b/src/run.bash
@@ -56,6 +56,8 @@
 make clean
 time make
 make smoketest
+# TODO: this belongs elsewhere
+cp godoc $HOME/bin
 ) || exit $?
 
 (xcd ../doc/progs
diff --git a/usr/gri/pretty/Makefile b/usr/gri/pretty/Makefile
index 836f089..3546aaa 100644
--- a/usr/gri/pretty/Makefile
+++ b/usr/gri/pretty/Makefile
@@ -30,7 +30,7 @@
 clean:
 	rm -f pretty untab godoc *.6 *.a 6.out *~
 
-godoc.6:	docprinter.6 compilation.6
+godoc.6:	docprinter.6 compilation.6 comment.6
 
 pretty.6:	 platform.6 astprinter.6 compilation.6
 
@@ -44,5 +44,7 @@
 
 docprinter.6:	astprinter.6
 
+comment.6:
+
 %.6:	%.go
 	$(G) $(F) $<
diff --git a/usr/gri/pretty/comment.go b/usr/gri/pretty/comment.go
new file mode 100644
index 0000000..1025856
--- /dev/null
+++ b/usr/gri/pretty/comment.go
@@ -0,0 +1,215 @@
+// 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 comment -> HTML formatting
+
+package comment
+
+import (
+	"fmt";
+	"io";
+	"template";
+)
+
+// Split bytes into lines.
+func split(text []byte) [][]byte {
+	// count lines
+	n := 0;
+	last := 0;
+	for i, c := range text {
+		if c == '\n' {
+			last = i+1;
+			n++;
+		}
+	}
+	if last < len(text) {
+		n++;
+	}
+
+	// split
+	out := make([][]byte, n);
+	last = 0;
+	n = 0;
+	for i, c := range text {
+		if c == '\n' {
+			out[n] = text[last : i+1];
+			last = i+1;
+			n++;
+		}
+	}
+	if last < len(text) {
+		out[n] = text[last : len(text)];
+	}
+
+	return out;
+}
+
+
+var (
+	ldquo = io.StringBytes("&ldquo;");
+	rdquo = io.StringBytes("&rdquo;");
+)
+
+// Escape comment text for HTML.
+// Also, turn `` into &ldquo; and '' into &rdquo;.
+func commentEscape(w io.Write, s []byte) {
+	last := 0;
+	for i := 0; i < len(s)-1; i++ {
+		if s[i] == s[i+1] && (s[i] == '`' || s[i] == '\'') {
+			template.HtmlEscape(w, s[last : i]);
+			last = i+2;
+			switch s[i] {
+			case '`':
+				w.Write(ldquo);
+			case '\'':
+				w.Write(rdquo);
+			}
+			i++;	// loop will add one more
+		}
+	}
+	template.HtmlEscape(w, s[last : len(s)]);
+}
+
+
+var (
+	html_p = io.StringBytes("<p>\n");
+	html_endp = io.StringBytes("</p>\n");
+	html_pre = io.StringBytes("<pre>");
+	html_endpre = io.StringBytes("</pre>\n");
+)
+
+
+func indentLen(s []byte) int {
+	i := 0;
+	for i < len(s) && (s[i] == ' ' || s[i] == '\t') {
+		i++;
+	}
+	return i;
+}
+
+
+func isBlank(s []byte) bool {
+	return len(s) == 0 || (len(s) == 1 && s[0] == '\n')
+}
+
+
+func commonPrefix(a, b []byte) []byte {
+	i := 0;
+	for i < len(a) && i < len(b) && a[i] == b[i] {
+		i++;
+	}
+	return a[0 : i];
+}
+
+
+func unindent(block [][]byte) {
+	if len(block) == 0 {
+		return;
+	}
+
+	// compute maximum common white prefix
+	prefix := block[0][0 : indentLen(block[0])];
+	for i, line := range block {
+		if !isBlank(line) {
+			prefix = commonPrefix(prefix, line[0 : indentLen(line)]);
+		}
+	}
+	n := len(prefix);
+
+	// remove
+	for i, line := range block {
+		if !isBlank(line) {
+			block[i] = line[n : len(line)];
+		}
+	}
+}
+
+
+// Convert comment text to formatted HTML.
+// The comment was prepared by DocReader,
+// so it is known not to have leading, trailing blank lines
+// nor to have trailing spaces at the end of lines.
+// The comment markers have already been removed.
+//
+// Turn each run of multiple \n into </p><p>
+// Turn each run of indented lines into <pre> without indent.
+//
+// TODO(rsc): I'd like to pass in an array of variable names []string
+// and then italicize those strings when they appear as words.
+func ToHtml(w io.Write, s []byte) {
+	inpara := false;
+
+	/* TODO(rsc): 6g cant generate code for these
+	close := func() {
+		if inpara {
+			w.Write(html_endp);
+			inpara = false;
+		}
+	};
+	open := func() {
+		if !inpara {
+			w.Write(html_p);
+			inpara = true;
+		}
+	};
+	*/
+
+	lines := split(s);
+	unindent(lines);
+	for i := 0; i < len(lines);  {
+		line := lines[i];
+		if isBlank(line) {
+			// close paragraph
+			if inpara {
+				w.Write(html_endp);
+				inpara = false;
+			}
+			i++;
+			continue;
+		}
+		if indentLen(line) > 0 {
+			// close paragraph
+			if inpara {
+				w.Write(html_endp);
+				inpara = false;
+			}
+
+			// count indented or blank lines
+			j := i+1;
+			for j < len(lines) && (isBlank(lines[j]) || indentLen(lines[j]) > 0) {
+				j++;
+			}
+			// but not trailing blank lines
+			for j > i && isBlank(lines[j-1]) {
+				j--;
+			}
+			block := lines[i : j];
+			i = j;
+
+			unindent(block);
+
+			// put those lines in a pre block.
+			// they don't get the nice text formatting,
+			// just html escaping
+			w.Write(html_pre);
+			for k, line := range block {
+				template.HtmlEscape(w, line);
+			}
+			w.Write(html_endpre);
+			continue;
+		}
+		// open paragraph
+		if !inpara {
+			w.Write(html_p);
+			inpara = true;
+		}
+		commentEscape(w, lines[i]);
+		i++;
+	}
+	if inpara {
+		w.Write(html_endp);
+		inpara = false;
+	}
+}
+
diff --git a/usr/gri/pretty/docprinter.go b/usr/gri/pretty/docprinter.go
index 623c516..9672eb0 100644
--- a/usr/gri/pretty/docprinter.go
+++ b/usr/gri/pretty/docprinter.go
@@ -2,12 +2,18 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package docPrinter
+// TODO: printing is gone; install as "go/doc"
+
+package doc
 
 import (
 	"ast";
 	"fmt";
 	"io";
+	"once";
+	"regexp";
+	"sort";
+	"strings";
 	"token";
 	"unicode";
 	"utf8";
@@ -20,16 +26,9 @@
 // ----------------------------------------------------------------------------
 // Elementary support
 
-// TODO this should be an AST method
-func isExported(name *ast.Ident) bool {
-	ch, len := utf8.DecodeRuneInString(name.Value, 0);
-	return unicode.IsUpper(ch);
-}
-
-
 func hasExportedNames(names []*ast.Ident) bool {
 	for i, name := range names {
-		if isExported(name) {
+		if name.IsExported() {
 			return true;
 		}
 	}
@@ -48,48 +47,34 @@
 
 // ----------------------------------------------------------------------------
 
-type valueDoc struct {
-	decl *ast.GenDecl;  // len(decl.Specs) >= 1, and the element type is *ast.ValueSpec
-}
-
-
-type funcDoc struct {
-	decl *ast.FuncDecl;
-}
-
-
 type typeDoc struct {
 	decl *ast.GenDecl;  // len(decl.Specs) == 1, and the element type is *ast.TypeSpec
-	factories map[string] *funcDoc;
-	methods map[string] *funcDoc;
+	factories map[string] *ast.FuncDecl;
+	methods map[string] *ast.FuncDecl;
 }
 
 
-type PackageDoc struct {
+// DocReader accumulates documentation for a single package.
+type DocReader struct {
 	name string;  // package name
+	path string;  // import path
 	doc ast.Comments;  // package documentation, if any
-	consts *vector.Vector;  // list of *valueDoc
+	consts *vector.Vector;  // list of *ast.GenDecl
 	types map[string] *typeDoc;
-	vars *vector.Vector;  // list of *valueDoc
-	funcs map[string] *funcDoc;
+	vars *vector.Vector;  // list of *ast.GenDecl
+	funcs map[string] *ast.FuncDecl;
 }
 
 
-func (doc *PackageDoc) PackageName() string {
-	return doc.name;
-}
-
-
-// PackageDoc initializes a document to collect package documentation.
-// The package name is provided as initial argument. Use AddPackage to
-// add the AST for each source file belonging to the same package.
-//
-func (doc *PackageDoc) Init(name string) {
-	doc.name = name;
+// Init initializes a DocReader to collect package documentation
+// for the package with the given package name and import path.
+func (doc *DocReader) Init(pkg, imp string) {
+	doc.name = pkg;
+	doc.path = imp;
 	doc.consts = vector.New(0);
 	doc.types = make(map[string] *typeDoc);
 	doc.vars = vector.New(0);
-	doc.funcs = make(map[string] *funcDoc);
+	doc.funcs = make(map[string] *ast.FuncDecl);
 }
 
 
@@ -104,7 +89,7 @@
 }
 
 
-func (doc *PackageDoc) lookupTypeDoc(typ ast.Expr) *typeDoc {
+func (doc *DocReader) lookupTypeDoc(typ ast.Expr) *typeDoc {
 	tdoc, found := doc.types[baseTypeName(typ)];
 	if found {
 		return tdoc;
@@ -113,18 +98,17 @@
 }
 
 
-func (doc *PackageDoc) addType(decl *ast.GenDecl) {
+func (doc *DocReader) addType(decl *ast.GenDecl) {
 	typ := decl.Specs[0].(*ast.TypeSpec);
 	name := typ.Name.Value;
-	tdoc := &typeDoc{decl, make(map[string] *funcDoc), make(map[string] *funcDoc)};
+	tdoc := &typeDoc{decl, make(map[string] *ast.FuncDecl), make(map[string] *ast.FuncDecl)};
 	doc.types[name] = tdoc;
 }
 
 
-func (doc *PackageDoc) addFunc(fun *ast.FuncDecl) {
+func (doc *DocReader) addFunc(fun *ast.FuncDecl) {
 	name := fun.Name.Value;
-	fdoc := &funcDoc{fun};
-	
+
 	// determine if it should be associated with a type
 	var typ *typeDoc;
 	if fun.Recv != nil {
@@ -133,7 +117,7 @@
 		typ = doc.lookupTypeDoc(fun.Recv.Type);
 		if typ != nil {
 			// type found (i.e., exported)
-			typ.methods[name] = fdoc;
+			typ.methods[name] = fun;
 		}
 		// if the type wasn't found, it wasn't exported
 		// TODO: a non-exported type may still have exported functions
@@ -149,18 +133,18 @@
 			// exactly one (named or anonymous) result type
 			typ = doc.lookupTypeDoc(res.Type);
 			if typ != nil {
-				typ.factories[name] = fdoc;
+				typ.factories[name] = fun;
 				return;
 			}
 		}
 	}
 
 	// ordinary function
-	doc.funcs[name] = fdoc;
+	doc.funcs[name] = fun;
 }
 
 
-func (doc *PackageDoc) addDecl(decl ast.Decl) {
+func (doc *DocReader) addDecl(decl ast.Decl) {
 	switch d := decl.(type) {
 	case *ast.GenDecl:
 		if len(d.Specs) > 0 {
@@ -170,13 +154,13 @@
 			case token.CONST:
 				// constants are always handled as a group
 				if hasExportedSpecs(d.Specs) {
-					doc.consts.Push(&valueDoc{d});
+					doc.consts.Push(d);
 				}
 			case token.TYPE:
 				// types are handled individually
 				for i, spec := range d.Specs {
 					s := spec.(*ast.TypeSpec);
-					if isExported(s.Name) {
+					if s.Name.IsExported() {
 						// make a (fake) GenDecl node for this TypeSpec
 						// (we need to do this here - as opposed to just
 						// for printing - so we don't loose the GenDecl
@@ -188,23 +172,22 @@
 			case token.VAR:
 				// variables are always handled as a group
 				if hasExportedSpecs(d.Specs) {
-					doc.vars.Push(&valueDoc{d});
+					doc.vars.Push(d);
 				}
 			}
 		}
 	case *ast.FuncDecl:
-		if isExported(d.Name) {
+		if d.Name.IsExported() {
 			doc.addFunc(d);
 		}
 	}
 }
 
 
-// AddProgram adds the AST of a source file belonging to the same
-// package. The package names must match. If the source was added
-// before, AddProgram is a no-op.
+// AddProgram adds the AST for a source file to the DocReader.
+// Adding the same AST multiple times is a no-op.
 //
-func (doc *PackageDoc) AddProgram(prog *ast.Program) {
+func (doc *DocReader) AddProgram(prog *ast.Program) {
 	if doc.name != prog.Name.Value {
 		panic("package names don't match");
 	}
@@ -221,254 +204,327 @@
 	}
 }
 
-
 // ----------------------------------------------------------------------------
-// Printing
+// Conversion to external representation
 
-func htmlEscape(s []byte) []byte {
-	var buf io.ByteBuffer;
-	
-	i0 := 0;
-	for i := 0; i < len(s); i++ {
-		var esc string;
-		switch s[i] {
-		case '<': esc = "&lt;";
-		case '&': esc = "&amp;";
-		default: continue;
-		}
-		fmt.Fprintf(&buf, "%s%s", s[i0 : i], esc);
-		i0 := i+1;  // skip escaped char
+func Regexp(s string) *regexp.Regexp {
+	re, err := regexp.Compile(s);
+	if err != nil {
+		panic("MakeRegexp ", s, " ", err.String());
 	}
-
-	// write the rest
-	if i0 > 0 {
-		buf.Write(s[i0 : len(s)]);
-		s = buf.Data();
-	}
-	return s;
+	return re;
 }
 
 
-// Reduce contiguous sequences of '\t' in a string to a single '\t'.
-// This will produce better results when the string is printed via
-// a tabwriter.
-// TODO make this functionality optional.
-//
-func untabify(s []byte) []byte {
-	var buf io.ByteBuffer;
-
-	i0 := 0;
-	for i := 0; i < len(s); i++ {
-		if s[i] == '\t' {
-			i++;  // include '\t'
-			buf.Write(s[i0 : i]);
-			// skip additional tabs
-			for i < len(s) && s[i] == '\t' {
-				i++;
-			}
-			i0 := i;
-		} else {
-			i++;
-		}
-	}
-
-	// write the rest
-	if i0 > 0 {
-		buf.Write(s[i0 : len(s)]);
-		s = buf.Data();
-	}
-	return s;
-}
-
-
-func stripCommentDelimiters(s []byte) []byte {
-	switch s[1] {
-	case '/': return s[2 : len(s)-1];
-	case '*': return s[2 : len(s)-2];
-	}
-	panic();
-	return nil;
-}
-
-
-const /* formatting mode */ (
-	in_gap = iota;
-	in_paragraph;
-	in_preformatted;
+var (
+	comment_markers *regexp.Regexp;
+	trailing_whitespace *regexp.Regexp;
+	comment_junk *regexp.Regexp;
 )
 
-func printLine(p *astPrinter.Printer, line []byte, mode int) int {
-	// If a line starts with " *" (as a result of a vertical /****/ comment),
-	// strip it away. For an example of such a comment, see src/lib/flag.go.
-	if len(line) >= 2 && line[0] == ' ' && line[1] == '*' {
-		line = line[2 : len(line)];
-	}
-
-	// The line is indented if it starts with a tab.
-	// In either case strip away a leading space or tab.
-	indented := false;
-	if len(line) > 0 {
-		switch line[0] {
-		case '\t':
-			indented = true;
-			fallthrough;
-		case ' ':
-			line = line[1 : len(line)];
-		}
-	}
-
-	if len(line) == 0 {
-		// empty line
-		switch mode {
-		case in_paragraph:
-			p.Printf("</p>\n");
-			mode = in_gap;
-		case in_preformatted:
-			p.Printf("\n");
-			// remain in preformatted
-		}
-	} else {
-		// non-empty line
-		if indented {
-			switch mode {
-			case in_gap:
-				p.Printf("<pre>\n");
-			case in_paragraph:
-				p.Printf("</p>\n");
-				p.Printf("<pre>\n");
-			}
-			mode = in_preformatted;
-		} else {
-			switch mode {
-			case in_gap:
-				p.Printf("<p>\n");
-			case in_preformatted:
-				p.Printf("</pre>\n");
-				p.Printf("<p>\n");
-			}
-			mode = in_paragraph;
-		}
-		// print line
-		p.Printf("%s\n", untabify(htmlEscape(line)));
-	}
-	return mode;
+// TODO(rsc): Cannot use var initialization for regexps,
+// because Regexp constructor needs threads.
+func SetupRegexps() {
+	comment_markers = Regexp("^[ \t]*(// ?| ?\\* ?)");
+	trailing_whitespace = Regexp("[ \t\r]+$");
+	comment_junk = Regexp("^[ \t]*(/\\*|\\*/)[ \t]*$");
 }
 
 
-func closeMode(p *astPrinter.Printer, mode int) {
-	switch mode {
-	case in_paragraph:
-		p.Printf("</p>\n");
-	case in_preformatted:
-		p.Printf("</pre>\n");
+// Aggregate comment text, without comment markers.
+func comment(comments ast.Comments) string {
+	once.Do(SetupRegexps);
+	lines := make([]string, 0, 20);
+	for i, c := range comments {
+		// split on newlines
+		cl := strings.Split(string(c.Text), "\n");
+
+		// walk lines, stripping comment markers
+		w := 0;
+		for j, l := range cl {
+			// remove /* and */ lines
+			if comment_junk.Match(l) {
+				continue;
+			}
+
+			// strip trailing white space
+			m := trailing_whitespace.Execute(l);
+			if len(m) > 0 {
+				l = l[0 : m[1]];
+			}
+
+			// strip leading comment markers
+			m = comment_markers.Execute(l);
+			if len(m) > 0 {
+				l = l[m[1] : len(l)];
+			}
+
+			// throw away leading blank lines
+			if w == 0 && l == "" {
+				continue;
+			}
+
+			cl[w] = l;
+			w++;
+		}
+
+		// throw away trailing blank lines
+		for w > 0 && cl[w-1] == "" {
+			w--;
+		}
+		cl = cl[0 : w];
+
+		// add this comment to total list
+		// TODO: maybe separate with a single blank line
+		// if there is already a comment and len(cl) > 0?
+		for j, l := range cl {
+			n := len(lines);
+			if n+1 >= cap(lines) {
+				newlines := make([]string, n, 2*cap(lines));
+				for k := range newlines {
+					newlines[k] = lines[k];
+				}
+				lines = newlines;
+			}
+			lines = lines[0 : n+1];
+			lines[n] = l;
+		}
 	}
+
+	// add final "" entry to get trailing newline.
+	// loop always leaves room for one more.
+	n := len(lines);
+	lines = lines[0 : n+1];
+
+	return strings.Join(lines, "\n");
+}
+
+// ValueDoc is the documentation for a group of declared
+// values, either vars or consts.
+type ValueDoc struct {
+	Doc string;
+	Decl *ast.GenDecl;
+	order int;
+}
+
+type sortValueDoc []*ValueDoc
+func (p sortValueDoc) Len() int            { return len(p); }
+func (p sortValueDoc) Swap(i, j int)       { p[i], p[j] = p[j], p[i]; }
+
+func declName(d *ast.GenDecl) string {
+	if len(d.Specs) != 1 {
+		return ""
+	}
+
+	switch v := d.Specs[0].(type) {
+	case *ast.ValueSpec:
+		return v.Names[0].Value;
+	case *ast.TypeSpec:
+		return v.Name.Value;
+	}
+
+	return "";
+}
+
+func (p sortValueDoc) Less(i, j int) bool {
+	// sort by name
+	// pull blocks (name = "") up to top
+	// in original order
+	if ni, nj := declName(p[i].Decl), declName(p[j].Decl); ni != nj {
+		return ni < nj;
+	}
+	return p[i].order < p[j].order;
+}
+
+func makeValueDocs(v *vector.Vector) []*ValueDoc {
+	d := make([]*ValueDoc, v.Len());
+	for i := range d {
+		decl := v.At(i).(*ast.GenDecl);
+		d[i] = &ValueDoc{comment(decl.Doc), decl, i};
+	}
+	sort.Sort(sortValueDoc(d));
+	return d;
 }
 
 
-func printComments(p *astPrinter.Printer, comment ast.Comments) {
-	mode := in_gap;
-	for i, c := range comment {
-		s := stripCommentDelimiters(c.Text);
+// FuncDoc is the documentation for a func declaration,
+// either a top-level function or a method function.
+type FuncDoc struct {
+	Doc string;
+	Recv ast.Expr;	// TODO(rsc): Would like string here
+	Name string;
+	Decl *ast.FuncDecl;
+}
 
-		// split comment into lines and print the lines
- 		i0 := 0;  // beginning of current line
-		for i := 0; i < len(s); i++ {
-			if s[i] == '\n' {
-				// reached line end - print current line
-				mode = printLine(p, s[i0 : i], mode);
-				i0 = i + 1;  // beginning of next line; skip '\n'
+type sortFuncDoc []*FuncDoc
+func (p sortFuncDoc) Len() int            { return len(p); }
+func (p sortFuncDoc) Swap(i, j int)       { p[i], p[j] = p[j], p[i]; }
+func (p sortFuncDoc) Less(i, j int) bool  { return p[i].Name < p[j].Name; }
+
+func makeFuncDocs(m map[string] *ast.FuncDecl) []*FuncDoc {
+	d := make([]*FuncDoc, len(m));
+	i := 0;
+	for name, f := range m {
+		doc := new(FuncDoc);
+		doc.Doc = comment(f.Doc);
+		if f.Recv != nil {
+			doc.Recv = f.Recv.Type;
+		}
+		doc.Name = f.Name.Value;
+		doc.Decl = f;
+		d[i] = doc;
+		i++;
+	}
+	sort.Sort(sortFuncDoc(d));
+	return d;
+}
+
+
+// TypeDoc is the documentation for a declared type.
+// Factories is a sorted list of factory functions that return that type.
+// Methods is a sorted list of method functions on that type.
+type TypeDoc struct {
+	Doc string;
+	Type *ast.TypeSpec;
+	Factories []*FuncDoc;
+	Methods []*FuncDoc;
+	Decl *ast.GenDecl;
+	order int;
+}
+
+type sortTypeDoc []*TypeDoc
+func (p sortTypeDoc) Len() int            { return len(p); }
+func (p sortTypeDoc) Swap(i, j int)       { p[i], p[j] = p[j], p[i]; }
+func (p sortTypeDoc) Less(i, j int) bool {
+	// sort by name
+	// pull blocks (name = "") up to top
+	// in original order
+	if ni, nj := p[i].Type.Name.Value, p[j].Type.Name.Value; ni != nj {
+		return ni < nj;
+	}
+	return p[i].order < p[j].order;
+}
+
+// NOTE(rsc): This would appear not to be correct for type ( )
+// blocks, but the doc extractor above has split them into
+// individual statements.
+func makeTypeDocs(m map[string] *typeDoc) []*TypeDoc {
+	d := make([]*TypeDoc, len(m));
+	i := 0;
+	for name, old := range m {
+		typespec := old.decl.Specs[0].(*ast.TypeSpec);
+		t := new(TypeDoc);
+		t.Doc = comment(typespec.Doc);
+		t.Type = typespec;
+		t.Factories = makeFuncDocs(old.factories);
+		t.Methods = makeFuncDocs(old.methods);
+		t.Decl = old.decl;
+		t.order = i;
+		d[i] = t;
+		i++;
+	}
+	sort.Sort(sortTypeDoc(d));
+	return d;
+}
+
+
+// PackageDoc is the documentation for an entire package.
+type PackageDoc struct {
+	PackageName string;
+	ImportPath string;
+	Doc string;
+	Consts []*ValueDoc;
+	Types []*TypeDoc;
+	Vars []*ValueDoc;
+	Funcs []*FuncDoc;
+}
+
+
+// Doc returns the accumulated documentation for the package.
+func (doc *DocReader) Doc() *PackageDoc {
+	p := new(PackageDoc);
+	p.PackageName = doc.name;
+	p.ImportPath = doc.path;
+	p.Doc = comment(doc.doc);
+	p.Consts = makeValueDocs(doc.consts);
+	p.Vars = makeValueDocs(doc.vars);
+	p.Types = makeTypeDocs(doc.types);
+	p.Funcs = makeFuncDocs(doc.funcs);
+	return p;
+}
+
+
+// ----------------------------------------------------------------------------
+// Filtering by name
+
+func match(s string, a []string) bool {
+	for i, t := range a {
+		if s == t {
+			return true;
+		}
+	}
+	return false;
+}
+
+func matchDecl(d *ast.GenDecl, names []string) bool {
+	for i, d := range d.Specs {
+		switch v := d.(type) {
+		case *ast.ValueSpec:
+			for j, name := range v.Names {
+				if match(name.Value, names) {
+					return true;
+				}
+			}
+		case *ast.TypeSpec:
+			if match(v.Name.Value, names) {
+				return true;
 			}
 		}
-
-		// print last line
-		mode = printLine(p, s[i0 : len(s)], mode);
 	}
-	closeMode(p, mode);
+	return false;
 }
 
-
-func (c *valueDoc) print(p *astPrinter.Printer) {
-	printComments(p, c.decl.Doc);
-	p.Printf("<pre>");
-	p.DoGenDecl(c.decl);
-	p.Printf("</pre>\n");
-}
-
-
-func (f *funcDoc) print(p *astPrinter.Printer, hsize int) {
-	d := f.decl;
-	if d.Recv != nil {
-		p.Printf("<h%d>func (", hsize);
-		p.Expr(d.Recv.Type);
-		p.Printf(") %s</h%d>\n", d.Name.Value, hsize);
-	} else {
-		p.Printf("<h%d>func %s</h%d>\n", hsize, d.Name.Value, hsize);
-	}
-	p.Printf("<p><code>");
-	p.DoFuncDecl(d);
-	p.Printf("</code></p>\n");
-	printComments(p, d.Doc);
-}
-
-
-func (t *typeDoc) print(p *astPrinter.Printer) {
-	d := t.decl;
-	s := d.Specs[0].(*ast.TypeSpec);
-	p.Printf("<h2>type %s</h2>\n", s.Name.Value);
-	p.Printf("<p><pre>");
-	p.DoGenDecl(d);
-	p.Printf("</pre></p>\n");
-	printComments(p, s.Doc);
-	
-	// print associated methods, if any
-	for name, m := range t.factories {
-		m.print(p, 3);
-	}
-
-	for name, m := range t.methods {
-		m.print(p, 3);
-	}
-}
-
-
-func (doc *PackageDoc) Print(writer io.Write) {
-	var p astPrinter.Printer;
-	p.Init(writer, nil, nil, true);
-	
-	// program header
-	fmt.Fprintf(writer, "<h1>package %s</h1>\n", doc.name);
-	fmt.Fprintf(writer, "<p><code>import \"%s\"</code></p>\n", doc.name);
-	printComments(&p, doc.doc);
-
-	// constants
-	if doc.consts.Len() > 0 {
-		fmt.Fprintln(writer, "<hr />");
-		fmt.Fprintln(writer, "<h2>Constants</h2>");
-		for i := 0; i < doc.consts.Len(); i++ {
-			doc.consts.At(i).(*valueDoc).print(&p);
+func filterValueDocs(a []*ValueDoc, names []string) []*ValueDoc {
+	w := 0;
+	for i, vd := range a {
+		if matchDecl(vd.Decl, names) {
+			a[w] = vd;
+			w++;
 		}
 	}
-
-	// variables
-	if doc.vars.Len() > 0 {
-		fmt.Fprintln(writer, "<hr />");
-		fmt.Fprintln(writer, "<h2>Variables</h2>");
-		for i := 0; i < doc.vars.Len(); i++ {
-			doc.vars.At(i).(*valueDoc).print(&p);
-		}
-	}
-
-	// functions
-	if len(doc.funcs) > 0 {
-		fmt.Fprintln(writer, "<hr />");
-		for name, f := range doc.funcs {
-			f.print(&p, 2);
-		}
-	}
-
-	// types
-	for name, t := range doc.types {
-		fmt.Fprintln(writer, "<hr />");
-		t.print(&p);
-	}
+	return a[0 : w];
 }
+
+func filterTypeDocs(a []*TypeDoc, names []string) []*TypeDoc {
+	w := 0;
+	for i, td := range a {
+		if matchDecl(td.Decl, names) {
+			a[w] = td;
+			w++;
+		}
+	}
+	return a[0 : w];
+}
+
+func filterFuncDocs(a []*FuncDoc, names []string) []*FuncDoc {
+	w := 0;
+	for i, fd := range a {
+		if match(fd.Name, names) {
+			a[w] = fd;
+			w++;
+		}
+	}
+	return a[0 : w];
+}
+
+// Filter eliminates information from d that is not
+// about one of the given names.
+// TODO: Recognize "Type.Method" as a name.
+func (p *PackageDoc) Filter(names []string) {
+	p.Consts = filterValueDocs(p.Consts, names);
+	p.Vars = filterValueDocs(p.Vars, names);
+	p.Types = filterTypeDocs(p.Types, names);
+	p.Funcs = filterFuncDocs(p.Funcs, names);
+	p.Doc = "";	// don't show top-level package doc
+}
+
diff --git a/usr/gri/pretty/godoc.go b/usr/gri/pretty/godoc.go
index 699d820..54e0e1d 100644
--- a/usr/gri/pretty/godoc.go
+++ b/usr/gri/pretty/godoc.go
@@ -6,8 +6,8 @@
 
 // Web server tree:
 //
-//	http://godoc/	main landing page (TODO)
-//	http://godoc/doc/	serve from $GOROOT/doc - spec, mem, tutorial, etc. (TODO)
+//	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
@@ -48,7 +48,8 @@
 	"vector";
 
 	"astprinter";
-	"docprinter";
+	"comment";
+	"docprinter";	// TODO: "doc"
 )
 
 
@@ -57,18 +58,11 @@
 // - 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"
 
-
-const (
-	docPrefix = "/doc/";
-	filePrefix = "/file/";
-)
-
-
 var (
 	goroot string;
 
@@ -80,8 +74,15 @@
 	// 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;
@@ -101,28 +102,12 @@
 }
 
 
-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, "<a href=\"%s\">%s</a><br />\n", pathutil.Clean(filePrefix + dir + "/" + name), name);
-}
-
-
 func makeTabwriter(writer io.Write) *tabwriter.Writer {
 	padchar := byte(' ');
 	if *usetabs {
@@ -132,63 +117,97 @@
 }
 
 
+// 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 parseError struct {
+type rawError 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;
+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 (h *errorHandler) Error(pos token.Position, msg string) {
+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
-	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});
+	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(path string, mode uint) (*ast.Program, errorList) {
-	src, err := os.Open(path, os.O_RDONLY, 0);
-	defer src.Close();
+func parse(filename string, mode uint) (*ast.Program, *parseErrors) {
+	src, err := ReadFile(filename);
 	if err != nil {
-		log.Stderrf("open %s: %v", path, err);
-		var noPos token.Position;
-		return nil, errorList{parseError{noPos, err.String()}};
+		log.Stderrf("ReadFile %s: %v", filename, err);
+		errs := []parseError{parseError{nil, 0, err.String()}};
+		return nil, &parseErrors{filename, errs, nil};
 	}
 
-	var handler errorHandler;
-	prog, ok := parser.Parse(src, &handler, mode);
+	var raw rawErrorVector;
+	prog, ok := parser.Parse(src, &raw, 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 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;
 		}
-		sort.Sort(errors);
-		return nil, errors;
+		errs[raw.Len()].src = src[offs : len(src)];
+		return nil, &parseErrors{filename, errs, src};
 	}
 
 	return prog, nil;
@@ -198,56 +217,162 @@
 // ----------------------------------------------------------------------------
 // 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);
-	}
+// Return text for decl.
+func DeclText(d ast.Decl) []byte {
 	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());
+	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{}) {
-	once.Do(readTemplate);
-
-	c.SetHeader("content-type", "text/html; charset=utf-8");
-
 	type Data struct {
-		title string;
-		header string;
+		title interface{};
+		header interface{};
 		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);
+		content interface{};
 	}
 
 	var d Data;
-	d.title = title.(string);
-	d.header = title.(string);
+	d.title = title;
+	d.header = title;
 	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);
-	}
+	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);
 }
 
 
@@ -257,120 +382,20 @@
 
 
 // ----------------------------------------------------------------------------
-// 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, "<h2>Directories</h2>");
-	for i, entry := range list {
-		if entry.IsDirectory() {
-			printLink(&b, path, entry.Name);
-		}
-	}
-
-	fmt.Fprintln(&b, "<h2>Go files</h2>");
-	for i, entry := range list {
-		if isGoFile(&entry) {
-			printLink(&b, path, entry.Name);
-		}
-	}
-
-	fmt.Fprintln(&b, "<h2>Other files</h2>");
-	for i, entry := range list {
-		if !entry.IsDirectory() && !isGoFile(&entry) {
-			fmt.Fprintf(&b, "%s<br />\n", entry.Name);
-		}
-	}
-
-	servePage(c, dirname + " - Contents", b.Data());
-}
-
-
-// ----------------------------------------------------------------------------
 // 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();
-
-	// generate body
+func serveParseErrors(c *http.Conn, errors *parseErrors) {
+	// format errors
 	var b io.ByteBuffer;
-	// section title
-	fmt.Fprintf(&b, "<h1>Parse errors in %s</h1>\n", filename);
-
-	// handle read errors
-	if err1 != nil || err2 != nil {
-		fmt.Fprintf(&b, "could not read file %s\n", filename);
-		return;
-	}
-
-	// write source with error messages interspersed
-	fmt.Fprintln(&b, "<pre>");
-	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, "<b><font color=red>%s >>></font></b>", 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, "</pre>");
-
-	servePage(c, filename, b.Data());
+	parseerrorHtml.Execute(errors, &b);
+	servePage(c, errors.filename + " - Parse Errors", 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);
+func serveGoSource(c *http.Conn, name string) {
+	prog, errors := parse(name, parser.ParseComments);
+	if errors != nil {
+		serveParseErrors(c, errors);
 		return;
 	}
 
@@ -383,40 +408,30 @@
 	writer.Flush();  // ignore errors
 	fmt.Fprintln(&b, "</pre>");
 
-	servePage(c, path + " - Go source", b.Data());
+	servePage(c, name + " - 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
-	}
-}
+var fileServer = http.FileServer(".", "");
 
-
-func serveFile(c *http.Conn, path string) {
-	dir, err := os.Stat(path);
-	if err != nil {
-		serveError(c, err.String(), path);
-		return;
-	}
-
+func serveFile(c *http.Conn, req *http.Request) {
+	// pick off special cases and hand the rest to the standard file server
 	switch {
-	case dir.IsDirectory():
-		serveDir(c, path);
-	case isGoFile(dir):
-		serveGoSource(c, ".", path);
-	case isHTMLFile(dir):
-		serveHTMLFile(c, path);
+	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:
-		serveError(c, "Not a directory or .go file", path);
+		fileServer.ServeHTTP(c, req);
 	}
 }
 
@@ -427,6 +442,7 @@
 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
 }
 
@@ -437,7 +453,7 @@
 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) {
+func addFile(pmap map[string]*pakDesc, dirname, filename, importprefix string) {
 	if strings.HasSuffix(filename, "_test.go") {
 		// ignore package tests
 		return;
@@ -452,14 +468,21 @@
 		// ignore main packages for now
 		return;
 	}
-	pakname := pathutil.Clean(dirname + "/" + prog.Name.Value);
+
+	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[pakname];
+	pakdesc, found := pmap[importpath];
 	if !found {
 		// add a new descriptor
-		pakdesc = &pakDesc{dirname, prog.Name.Value, make(map[string]bool)};
-		pmap[pakname] = pakdesc;
+		pakdesc = &pakDesc{dirname, prog.Name.Value, importpath, make(map[string]bool)};
+		pmap[importpath] = pakdesc;
 	}
 
 	//fmt.Printf("pak = %s, file = %s\n", pakname, filename);
@@ -472,7 +495,7 @@
 }
 
 
-func addDirectory(pmap map[string]*pakDesc, dirname string) {
+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 {
@@ -486,11 +509,24 @@
 		return;
 	}
 
+	nsub := 0;
 	for i, entry := range list {
 		switch {
 		case isGoFile(&entry):
-			//fmt.Printf("found %s/%s\n", dirname, entry.Name);
-			addFile(pmap, dirname, entry.Name);
+			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++;
+			}
 		}
 	}
 }
@@ -509,53 +545,67 @@
 }
 
 
-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++;
-	}
-
+func (p *pakDesc) Doc() (*doc.PackageDoc, *parseErrors) {
 	// compute documentation
-	var doc docPrinter.PackageDoc;
-	for i, filename := range filenames {
+	var r doc.DocReader;
+	i := 0;
+	for filename := range p.filenames {
 		path := p.dirname + "/" + filename;
-		prog, errors := parse(path, parser.ParseComments);
-		if len(errors) > 0 {
-			serveParseErrors(c, filename, errors);
-			return;
+		prog, err := parse(path, parser.ParseComments);
+		if err != nil {
+			return nil, err;
 		}
 
 		if i == 0 {
-			// first package - initialize docPrinter
-			doc.Init(prog.Name.Value);
+			// first file - initialize doc
+			r.Init(prog.Name.Value, p.importpath);
 		}
-		doc.AddProgram(prog);
+		i++;
+		r.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());
+	return r.Doc(), nil;
 }
 
 
-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, "<a href=\"%s\">%s</a> <font color=grey>(%s)</font><br />\n",
-			p.pakname, p.pakname, link);
+func servePackage(c *http.Conn, p *pakDesc) {
+	doc, errors := p.Doc();
+	if errors != nil {
+		serveParseErrors(c, errors);
+		return;
 	}
 
-	servePage(c, "Packages", b.Data());
+	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());
+}
 
-	// TODO: show subdirectories
+
+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());
 }
 
 
@@ -568,50 +618,73 @@
 //	"math"	- single package made up of directory
 //	"container"	- directory listing
 //	"container/vector"	- single package in container directory
-func findPackages(name string) (*pakDesc, pakArray) {
+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);
-	dir := pathutil.Clean("src/lib/" + name);
+	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);
+		addDirectory(pmap, dir, cname, &info.Subdirs);
 		paks := mapValues(pmap);
 		if len(paks) == 1 {
 			p := paks[0];
 			if p.dirname == dir && p.pakname == pak {
-				return p, nil;
+				info.Package = p;
+				info.Path = cname;
+				return info;
 			}
 		}
-		return nil, paks;
+		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);
-	addDirectory(pmap, parentdir);
-	if p, ok := pmap[dir]; ok {
-		return p, nil;
+	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;
 	}
 
-	return nil, nil;
+	info.Path = name;	// original, uncleaned name
+	return info;
 }
 
 
-func servePkg(c *http.Conn, path string) {
-	pak, paks := findPackages(path);
+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;
+	}
 
-	// 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);
+	if info.Package != nil {
+		servePackage(c, info.Package);
+	} else {
+		servePackageList(c, info);
 	}
 }
 
@@ -619,25 +692,11 @@
 // ----------------------------------------------------------------------------
 // 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.Stderrf("%s\t%s", req.Host, path);
-		}
-		handler(c, path[len(prefix) : len(path)]);
-	};
-
-	// install the customized handler
-	http.Handle(prefix, http.HandlerFunc(f));
+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);
+	})
 }
 
 
@@ -666,23 +725,54 @@
 		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);
 		}
 
-		installHandler("/mem", makeFixedFileServer("doc/go_mem.html"));
-		installHandler("/spec", makeFixedFileServer("doc/go_spec.html"));
-		installHandler("/pkg/", servePkg);
-		installHandler(filePrefix, serveFile);
+		http.Handle(Pkg, http.HandlerFunc(servePkg));
+		http.Handle("/", http.HandlerFunc(serveFile));
 
-		if err := http.ListenAndServe(*httpaddr, nil); err != nil {
+		if err := http.ListenAndServe(*httpaddr, handler); err != nil {
 			log.Exitf("ListenAndServe %s: %v", *httpaddr, err)
 		}
 		return;
 	}
 
-	log.Exitf("godoc command-line not implemented");
+	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);
 }